php反序列化
一些常用的知识
private变量会被序列化为:/x00类名/x00变量名 |
如何找反序列化链
一般来说,我们都是要通过反序列的链子实现某些操作,比如rce、文件包含、文件写入等等。可以通过从结果(目的、想到实现的操作)开始一直往前推,比较容易找到一条可用的链子。
简单样例:
|
分析:
最终利用的是
shen::__toString()
来rce,所以我们下一步是找能触发它的地方。在
big::__call()
的preg_match
能触发__toString
方法,可以理解为正则匹配是字符串操作,所以可以触发,因此我们用这个来触发shen::__toString()
。(注:把一个对象当作字符串处理可以触发__toString
方法)ctf::__destruct()
能触发__call
,因此用它来触发big::__call()
。(注:调用对象调用不存在的方法时触发__call
方法)__destruct
方法是在对象被销毁时触发,因此只要反序列化代码运行结束就能触发该方法。接下来是变量赋值的细节,首先
__call
的触发是用的是ctf
类的h1
变量,h2
变量会传递到__call
方法的$args
变量,因此h1
是new big()
,h2
是new shen()
;shen
类的cmd
变量是能执行命令的,因此里面是放想要执行的命令。最终的链子是:
ctf::__destruct => big::__call => shen::__toString
。
class ctf{
public $h1;
public $h2;
public function __destruct()
{
$this->h1->nonono($this->h2);
}
}
class big{
public function __call($name,$args){
if(preg_match('/ctf/i',$args[0])){
echo "gogogo";
}
}
}
class shen{
public $cmd = "phpinfo();";
public function __toString(){
echo "__toString";
eval($this->cmd);
}
}
$a = new ctf();
$a->h1 = new big();
$a->h2 = new shen();
echo serialize($a);
// O:3:"ctf":2:{s:2:"h1";O:3:"big":0:{}s:2:"h2";O:4:"shen":1:{s:3:"cmd";s:10:"phpinfo();";}}
wakeup()绕过总结
cve-2016-7124
影响范围:
- PHP5 < 5.6.25
- PHP7 < 7.0.10
正常来说在反序列化过程中,会先调用 __wakeup()
方法再进行 unserilize
的操作,但如果序列化字符串中表示对象属性个数的值大于真实的属性个数时,__wakeup()
的执行会被跳过。
样例(php 5.5.9):
|
正常反序列化:
GET: ?ctf=O:3:"ctf":2:{s:2:"h1";N;s:2:"h2";N;} |
很明显是先触发了wakeup
再触发destruct
。
若将"ctf":2
的2
改成3
,使得对象属性个数变大,则会使wakeup
不触发。
对象属性个数不匹配:
GET: ?ctf=O:3:"ctf":3:{s:2:"h1";N;s:2:"h2";N;} |
php引用赋值 &
在php里,我们可使用引用的方式让两个变量同时指向同一个内存地址,这样对其中一个变量操作时,另一个变量的值也会随之改变。
举例:
|
这个方法是用来绕过一些特定的判断。
样例(PHP 7.1.9):
|
若将$this->wakeup
和$this->key
引用关联起来,那么在__destruct
里对$this->key
修改时也会把$this->wakeup
一起修改了,从而达成if语句的条件。
这里并没有绕过wakeup
,wakeup
正常执行了,只不过利用了引用的特点,使得wakeup
里执行的操作对我们起不了作用。
pop:
|
PHP GC回收机制
PHP GC 回收机制是什么
在 PHP 中,是拥有垃圾回收机制 Garbage collection 的,也就是我们常说的 GC 机制的,在 PHP 中使用引用计数和回收周期来自动管理内存对象的,当一个变量被设置为 NULL ,或者没有任何指针指向时,它就会被变成垃圾,被 GC 机制自动回收掉;那么当一个对象没有了任何引用之后,就会被回收,在回收过程中,就会自动调用对象中的 __destruct()
方法。
- 上面这一段话我个人认为如果零基础看,会感觉到相当抽象。所以我们先来解读一下
PHP 引用计数
当我们 PHP 创建一个变量时,这个变量会被存储在一个名为 zval 的变量容器中。在这个 zval 变量容器中,不仅包含变量的类型和值,还包含两个字节的额外信息。
第一个字节名为 is_ref
,是 bool 值,它用来标识这个变量是否是属于引用集合。PHP 引擎通过这个字节来区分普通变量和引用变量,由于 PHP 允许用户使用 &
来使用自定义引用,zval 变量容器中还有一个内部引用计数机制,来优化内存使用。
第二个字节是 refcount
,它用来表示指向 zval 变量容器的变量个数。所有的符号存储在一个符号表中,其中每个符号都有作用域。
接下来看看例子:
容器的生成:
|
这里定义了一个变量 $a
,生成了类型为 String 和值为 test
的变量容器,而对于两个额外的字节,is_ref
和 refcount
,我们这里可以看到是不存在引用的,所以 is_ref
的值应该是 false,而 refcount
是表示变量个数的,那么这里就应该是1,验证一下。
接下来添加一个引用
|
这里的话 a 的 refcount
应该是 1,is_ref
应该是 true,验证一下
结果不同于我们所想的,这是为什么呢?
因为同一变量容器被变量 a 和变量 b 关联,当没必要时,php 不会去复制已生成的变量容器。 所以这一个 zval
容器存储了 a 和 b 两个变量,就使得 refcount
的值为 2。
容器的销毁:
当函数执行结束或者对变量调用了 unset()
函数,refcount 就会减 1。
|
PHP GC 回收机制攻击面
- 原理:当
is_ref
减少时,会触发__destuct
魔术方法,由此产生的一些 trick 类型攻击
变量被unset
函数处理:
|
当对象为NULL
时也是可以触发__destruct
的。
在一个 array 里面存在一个键值对,value 为某个类,当这个类为 NULL 的时候,会被认为是 is_ref
为 0,也就是 false。这就可以触发到 __destruct
方法
样例:
|
这里因为有异常处理,所以正常情况下是无法__destruct
,这时我们就需要利用GC回收机制来触发__destruct
。
|
得到序列化文本如下
a:2:{s:1:"a";O:1:"B":0:{}s:1:"b";N;} |
这时我们将键名b
改成a
,即在反序列化时,会下先让a
赋值为类B
,之后再将a
赋值为NULL
,但一开始a
已经是对象了,赋值为NULL
时就会出现对象为NULL
的情况,从而触发__destruct
。
a:2:{s:1:"a";O:1:"B":0:{}s:1:"a";N;} |
这个是在反序列化中经常使用的方法。
fast destruct
知识点:
1、PHP中,如果单独执行unserialize函数进行常规的反序列化,那么被反序列化后的整个对象的生命周期就仅限于这个函数执行的生命周期,当这个函数执行完毕,这个类就没了,在有析构函数的情况下就会执行它。
2、PHP中,如果用一个变量接住反序列化函数的返回值,那么被反序列化的对象其生命周期就会变长,由于它一直都存在于这个变量当中,那么在PHP脚本走完流程之后,这个对象才会被销毁,在有析构函数的情况下就会将其执行。
fast destruct 就是用来解决第二种情况带来的问题的。
解法一
当需要构造pop链的时候,反序列化后的对象被变量接住了,又要利用析构函数控制POP链的流程,这个时候就需要用到快速析构的技巧。
而且,大部分都是在类和类之间各有分工,在内部联系不是很紧密的情况下使用这种技巧。
使用索引相同的两个类,后一个类被反序列化时,前一个类会被销毁,从而调用析构函数(如果有的话)。
举例:
$unser = serialize(new array(0 => new class1(),1 => new class2(),2 => new class3(),1 => new class4(),2 => new class5())); |
那么整个流程如下:
class1被反序列化
class2被反序列化
class3被反序列化
class4被反序列化,class2被销毁
class5被反序列化,class3被销毁
注意,使用这种技巧得到的数组序列化字符串,其元素值必须依然是括号内元素的个数,只是对应的key要改一下。
“后一个类” 可以是一个无用的对象,包括数组。
其实就是一种GC回收机制。
解法二
在unserialize
过程中扫描器发现序列化字符串格式有误导致的提前异常退出,为了销毁之前建立的对象内存空间,会立刻调用对象的__destruct()
,提前触发反序列化链条。
这种情况只需要破坏原先的字符串格式即可,比如去掉最后的大括号,。
样例:
|
php issue#9618
php issue#9618提到了最新版wakeup()的一种bug,可以通过在反序列化后的字符串中包含字符串长度错误的变量名使反序列化在__wakeup之前调用__destruct()函数,最后绕过__wakeup(),版本:
- 7.4.x -7.4.30
- 8.0.x
样例(php 8.0.2):
|
payload:
|
很显然,wakeup
在最后触发。
使用C绕过
众所周知可以使用C进行绕过wakeup,但这样有一个缺点,就是你把O改为C后是没办法有属性的,那假如需要用属性命令执行就不行了。
新方法是用原生类把原本的类打包一下,生成以C开头的payload。
这也是绕过/^[Oa]:[/d]+/
过滤的方法之一。
样例(ctfshow 愚人杯3rd [easy_php]):
|
payload:
|
实现了unserialize接口类的大概率是C打头,列出一些以C开头的原生类:
ArrayObject::unserialize |
需要注意,这种方法在序列化时对php版本有要求,用7.3.4才可以输出以C开头的payload,换7.4或者8.0输出的就是O开头了,但在unserialize
时对php版本没要求。
字符串逃逸
当 PHP 中序列化后的数据进行了长度替换之后,就可能存在这一漏洞,即通过修改输入数据从而控制整个序列化的内容。
反序列化知识:
- PHP 在反序列化时,底层代码是以
;
作为字段的分隔,以}
作为结尾(字符串除外)并且是根据长度判断内容的- 注意点,很容易以为序列化后的字符串是
;}
结尾,实际上字符串序列化是以;}
结尾的,但对象序列化是直接}
结尾 - php反序列化字符逃逸,就是通过这个结尾符实现的
- 注意点,很容易以为序列化后的字符串是
- 当长度不对应的时候会出现报错
- 反序列化的过程是有一定识别范围的,在这个范围之外的字符都会被忽略,不影响反序列化的正常进行
字符变多
样例:
|
问:如果我能控制进行反序列化的字符串,该如何使$test2
的值是admin
而不是hacker
?
假如传入的test1
是xxx123
:
- 正常情况下序列化后的字符串为
O:1:"A":2:{s:5:"test1";s:6:"xxx123";s:5:"test2";s:6:"hacker";}
- 经过替换后,变成
O:1:"A":2:{s:5:"test1";s:6:"yyyyyy123";s:5:"test2";s:6:"hacker";}
- 反序列化时,读取
yyyyyy
后,发现没有闭合符号,因此反序列化失败 - 此时发现
123
逃逸出来了,这部分是可控的。
我们可以利用这可控的部分把test2
的值改掉。
这时候,需要先找到一个要逃逸那部分的字符串。
";s:5:"test2";s:5:"admin";}
是我们需要逃逸的部分。最简单的判断方法是先本地序列化出一个我们想要的序列化字符串O:1:"A":2:{s:5:"test1";s:6:"xxx123";s:5:"test2";s:5:"admin";}
,然后以xxx123
为分界线,取后边所有的字符串,这个字符串就是我们需要逃逸的部分。之后统计出
";s:5:"test2";s:5:"admin";}
的长度为27,说明我们需要逃逸27个字符。替换函数是将
x
替换成yy
,多出1个字符,因此输入27个x
,就能逃逸出27个字符。因此
test1
的值为xxxxxxxxxxxxxxxxxxxxxxxxxxx";s:5:"test2";s:5:"admin";}
字符减少
样例:
|
问:如果我能控制进行反序列化的字符串,该如何使$test3
的值是admin
而不是hacker
?
假如传入的test1
是xxxxxx
,test2
是abc
:
- 正常情况下序列化后的字符串为
O:1:"A":3:{s:5:"test1";s:6:"xxxxxx";s:5:"test2";s:3:"abc";s:5:"test3";s:6:"hacker";}
- 经过替换后,变成
O:1:"A":3:{s:5:"test1";s:6:"xxx";s:5:"test2";s:3:"abc";s:5:"test3";s:6:"hacker";}
- 反序列化时,读取
xxx";s
(长度为6)后,发现没有闭合符号,因此反序列化失败。 - 此时发现
";s
被吸收进test1
里了,这部分是可控的,可以控制吸收多少个字符。
我们可以利用这可控的部分把一些不需要的值吸收进去,再通过控制第二个变量,构造出我们想要的东西。
需要先找到一个要逃逸那部分的字符串
方法和
字符变多
一样,先本地序列化出一个我们想要的序列化字符串O:1:"A":2:{s:5:"test1";s:6:"xxx123";s:5:"test2";s:3:"abc";s:5:"test3";s:5:"admin";}
,然后以xxx123
为分界线,取后边所有的字符串,这个字符串就是我们需要逃逸的部分。";s:5:"test2";s:3:"abc";s:5:"test3";s:5:"admin";}
是需要逃逸的部分。接下来找需要吸收的部分,我们需要把
";s:5:"test2";s:3:"
吸收进来,这里的:3:
不是固定的,3
是上面逃逸部分的长度(49)。(这个不是重点)此时我们需要把
";s:5:"test2";s:49:"
吸收进来,让原本test2
的值成为反序列化字符串的合法部分。统计出
";s:5:"test2";s:49:"
的长度为20,说明我们需要吸收20个字符。替换函数是将
xx
替换成y
,减少1个字符,因此输入40个x
,就能吸收20个字符。因此传入
test1
的值为xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
,test2
的值为";s:5:"test2";N;s:5:"test3";s:5:"admin";}
Phar反序列化
Phar文件是php里类似于JAR的一种打包文件本质上是一种压缩文件,在PHP 5.3 或更高版本中默认开启,一个phar文件一个分为四部分
1.a stub |
生成phar文件
php内置了一个Phar类来处理相关操作
|
注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件
php一大部分的文件系统函数在通过phar://
伪协议解析phar文件时,都会将meta-data进行反序列化,基本上能使用伪协议的文件操作函数都能触发反序列化。
Phar反序列化漏洞利用
既然很多函数可以触发phar反序列化,那么接下来就要实际利用该漏洞
Phar反序列化不会调用 weakup
等方法
可以在不调用unserialize()
的情况下进行反序列化操作。
漏洞利用条件
- phar文件要能够上传到服务器端。
- 要有可用的魔术方法作为“跳板”。
- 文件操作函数的参数可控,且
:
、/
、phar
等特殊字符没有被过滤
phar文件生成样例:
|
小技巧/特性
十六进制绕过关键字
php反序列化是可以使用16进制的,只需要把类型小s
改成大S
。
样例:
|
t
的16进制是\74
,把s
改成S
时,便能将\74
解析成t
。
类名大小写不敏感
php对于类名大小写不敏感,也就是说,A
和a
都会认为是同一个类。
样例:
|
类内方法调用
样例代码:
|
静态调用类A
的test
方法:
A::test(); |
动态调用类A
的test
方法:
(new A())::test(); |
原生类相关
C开头
绕 wakeup
和绕 /^[Oa]:[/d]+/
过滤。
详细可以看 wakeup
绕过总结的 使用C绕过
。
以C开头的原生类:
ArrayObject::unserialize |
文件操作
遍历文件目录类
DirectoryIterator
FilesystemIterator
GlobIterator这三个都可以遍历文件目录,可以搭配伪协议使用。
可以用
glob://
且配合*
查找。注意,这几个类不能反序列化。
样例:
highlight_file(__FILE__);
$dir = new DirectoryIterator('.');
foreach($dir as $a ){
echo $a."<br>";
}读文件内容
SplFileObject
当用文件目录遍历到了敏感文件时,可以用SplFileObject类,SplFileInfo 类为单个文件的信息提供了一个高级的面向对象的接口,可以用于对文件内容的遍历、查找、操作。
样例:
highlight_file(__FILE__);
$text= new SplFileObject('./flag.txt');
foreach ($text as $tmp)
{
echo $tmp;
}
echo "</br>";
hash绕过和XSS实现
这里主要是用原生类Error
和Exception
来实现。
它们两个的用法是一样的,这里以Error
类作为例子讲解
Error中也有个__toString()
,可以控制它的内容实现字符串的输出。
Error可以传两个参数,有个参数值的不同则对象不同也就不相等,但对由于__toString()
返回的值相同md5和sha1加密后也相同,最后得到的数据也是一样的,所以可以达到hash绕过。
xss
样例:
highlight_file(__FILE__);
echo unserialize($_GET['ctf'])payload:
$a = new Error("<script>alert('xss test')</script>");
echo urlencode(serialize($a));
// O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A34%3A%22%3Cscript%3Ealert%28%27xss+test%27%29%3C%2Fscript%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A27%3A%22W%3A%5Cphpstudy_pro%5CWWW%5C114.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7Dhash
样例:
highlight_file(__FILE__);
$a = new Error("test",1);$b = new Error("test",2);
if($a !== $b)
{
echo '$a != $b'."</br>";
}
if(md5($a) === md5($b))
{
echo "md5相等";
}
SSRF
利用SoapClient
原生类的 __call
方法进行SSRF
。
使用前提:
- 需要有soap扩展,且不是默认开启,需要手动开启
- 需要调用一个不存在的方法触发其__call()函数
- 仅限于http/https协议
正常情况下的SoapClient
类,调用一个不存在的函数,会去调用__call
方法。
CRLF攻击
什么是CRLF,其实就是回车和换行造成的漏洞,十六进制为0x0d,0x0a
,在HTTP当中header
和body
之间就是两个CRLF分割的,所以如果我们能够控制HTTP消息头中的字符,注入一些恶意的换行,这样就能注入一些会话cookie和html代码,所以crlf injection 又叫做 HTTP Response Splitting。
SoapClient
在调用发送数据时,存在CRLF漏洞,因此我们可以通过控制其中一个http头来构造出我们想要的请求包。
Content-Length
是HTTP消息长度,它指定多少个字符,就读取多少个字符,多余的字符会被丢弃,所以可以通过控制Content-Length
的长度,将没用的消息忽略掉。
使用SoapClient反序列化+CRLF 可以生成任意POST请求 。
exp:
|