ThinkPHP5漏洞分析之代码执行

漏洞概要

本次漏洞存在于 ThinkPHP 的缓存类中。该类会将缓存数据通过序列化的方式,直接存储在 .php 文件中,攻击者通过精心构造的 payload ,即可将 webshell 写入缓存文件。缓存文件的名字和目录均可预测出来,一旦缓存目录可访问或结合任意文件包含漏洞,即可触发 远程代码执行漏洞 。漏洞影响版本: 5.0.0<=ThinkPHP5<=5.0.10

漏洞环境

ThinkPHP5-5.0.10

application/index/controller/Index.php 文件代码设置如下:

php 复制代码
<?php
namespace app\index\controller;
use think\Cache;
class Index
{
    public function index()
    {
        Cache::set("name",input("get.username"));
        return 'Cache success';
    }
}

显示Cache success,没看懂

[192.168.10.128/tp5010/public/index.php?username=mochazz123%0d%0a@eval(_GET\[_\]);//](http://192.168.10.128/tp5010/public/index.php?username=mochazz123%0D%0A@eval%28GET%5B%5D%29;// "192.168.10.128/tp5010/public/index.php?username=mochazz123%0d%0a@eval($GET[]);//")

即可将 webshell 写入缓存文件。

可以看到缓存文件已经成功写入

漏洞分析

我们跟进 Cache 类的 set 方法,发现其先通过单例模式 init 方法,创建了一个类实例,该类由 cache 的配置项 type 决定,默认情况下其值为 File 。在本例中, self::$handler 即为 think\cache\driver\File 类实例。

thinkphp/library/think/cache/driver/ 目录下,我们可以看到 Thinkphp5 支持的几种缓存驱动类。我们接着上面的分析,程序调用 think\cache\driver\File 类的 set 方法。可以看到 data 数据没有经过任何处理,只是序列化后拼接存储在文件中,这里的 $this->options['data_compress'] 变量默认情况下为 false ,所以数据不会经过 gzcompress 函数处理。虽然在序列化数据前面拼接了单行注释符 // ,但是我们可以通过注入换行符绕过该限制。

现在我们就来看看缓存文件的名字是如何生成的。从上一张图片 第142行 ,我们可以看到文件名是通过调用 getCacheKey 方法获得的,我们跟进该方法。可以看到缓存文件的子目录和文件名均和缓存类设置的键有关(如本例中缓存类设置的键为 name )。程序先获得键名的 md5 值,然后将该 md5 值的前 2 个字符作为缓存子目录,后 30 字符作为缓存文件名。如果应用程序还设置了前缀 $this->options['prefix'] ,那么缓存文件还将多一个上级目录。

data:"s"表示字符串;20表示长度,

php 序列化的字母标识_php中 序列化字符串以o开头跟以c开头有什么区别-CSDN博客

serialize

(PHP 4, PHP 5, PHP 7, PHP 8)

serialize --- 生成值的可存储表示

说明

serialize (mixed $value): string

生成值的可存储表示。

这有利于存储或传递 PHP 的值,同时不丢失其类型和结构。

想要将已序列化的字符串变回 PHP 的值,可使用 unserialize()

参数

value

要序列化的值。serialize() 处理所有的类型,除了 resource 类型和一些 object(见下面的注释)。serialize() 甚至可以序列化包含对自身引用的数组。数组/对象内的循环引用也会被存储。其它任何引用都会丢失。

序列化对象时,PHP 将尝试在序列化之前调用成员函数 __serialize()__sleep()。这是为了允许对象在序列化之前进行最后一分钟的清理等等。同样,当使用 unserialize() 恢复对象时,会调用 __unserialize()__wakeup() 成员函数。

注意:

对象的 private 成员会在名前添加类名;protected 成员会在名前添加"*";这些前置值在两边都有 null 字节。

返回值

返回字符串,包含 value 的字节流表示,可以存储在任何地方。

注意这可能是包含 null 字节的二进制字符串,需要按原样存储和处理。例如,serialize() 的输出通常应该存储在数据库中 的 BLOB 字段,而不是 CHAR 或 TEXT 字段。

示例

<?php
// $sssion_data 是多维数组,包含当前用户的
// 会话信息。可以在请求结束时使用 serialize()
// 将其存储在数据库中。
$conn = odbc_connect("webdb", "php", "chicken");
$stmt = odbc_prepare($conn,
"UPDATE sessions SET data = ? WHERE id = ?");
$sqldata = array (serialize($session_data), $_SERVER['PHP_AUTH_USER']);
if (!odbc_execute($stmt, $sqldata)) {
$stmt = odbc_prepare($conn,
"INSERT INTO sessions (id, data) VALUES(?, ?)");
if (!odbc_execute($stmt, array_reverse($sqldata))) {
/* Something went wrong.. */
}
}
?>

注释

注意:

注意许多内置 PHP 对象不能序列化。然而,要么实现 Serializable 接口,要么实现 __serialize()/__unserialize()__sleep()/__wakeup() 魔术方法的则是可以的。如果内部类不满足这些其中任意一个,则就不能可靠的进行序列化。

上述规则有一些历史例外,一些内部对象可以在不实现接口或公开方法的情况下,使其序列化。

%0使用0来填充12位,

|-----|-----------------------|
| d | 参数视为整数并以(有符号)十进制数字呈现。 |

sprintf

PHP: sprintf - Manual

(PHP 4, PHP 5, PHP 7, PHP 8)

sprintf --- 返回格式化字符串

说明

sprintf (string $format, mixed ...$values): string

返回一个根据格式化字符串 format 生成的字符串。

示例 参数替换

支持按顺序用参数替换格式字符串里的占位符。

<?php
$num = 5;
$location = 'tree';

$format = 'There are %d monkeys in the %s';
echo sprintf($format, $num, $location);
?>

以上示例会输出:

复制代码
There are 5 monkeys in the tree

假设,我们想把它国际化,在一个单独的文件中创建格式字符串,我们将它重写为:

<?php
$format = 'The %s contains %d monkeys';
echo sprintf($format, $num, $location);
?>

我们现在有一个问题。 格式字符串中占位符的顺序与代码中参数的顺序不匹配。 我们希望保持代码原样,并在格式字符串中简单地指出占位符引用的参数。 我们可以这样写格式化字符串:

<?php
$format = 'The %2$s contains %1$d monkeys';
echo sprintf($format, $num, $location);
?>

另外一个好处是占位符可以重复使用,而无需在代码中添加更多参数。

<?php
$format = 'The %2$s contains %1$d monkeys.
That\'s a nice %2$s full of %1$d monkeys.';
echo sprintf($format, $num, $location);
?>

file_put_contents

(PHP 5, PHP 7, PHP 8)

file_put_contents --- 将数据写入文件

说明

file_put_contents (

string $filename,

mixed $data,

int $flags = 0,

?resource $context = null

): int|false

和依次调用 fopen()fwrite() 以及 fclose() 功能一样。

如果 filename 不存在,将会创建文件。反之,存在的文件将会重写,除非设置 FILE_APPEND flag。

参数

filename

要被写入数据的文件名。

data

要写入的数据。类型可以是 stringarray 或者是 stream 资源(如上面所说的那样)。

如果 data 指定为 stream 资源,这里 stream 中所保存的缓存数据将被写入到指定文件中,这种用法就相似于使用 stream_copy_to_stream() 函数。

参数 data 可以是数组(但不能为多维数组),这就相当于 file_put_contents($filename, join('', $array))

flags

flags 的值可以是 以下 flag 使用 OR (|) 运算符进行的组合。

Flag 描述
FILE_USE_INCLUDE_PATH 在 include 目录里搜索 filename。 更多信息可参见 include_path
FILE_APPEND 如果文件 filename 已经存在,追加数据而不是覆盖。
LOCK_EX 在写入时获取文件独占锁。换句话说,在调用 fopen()fwrite() 中间发生了 flock() 调用。这与调用带模式"x"的 fopen() 不同。
[Available flags]

context

一个 context 资源。

返回值

该函数将返回写入到文件内数据的字节数,失败时返回**false**

警告

此函数可能返回布尔值 false ,但也可能返回等同于 false 的非布尔值。请阅读 布尔类型章节以获取更多信息。应使用 === 运算符来测试此函数的返回值。

示例

<?php
$file = 'people.txt';
// 打开文件获取已经存在的内容
$current = file_get_contents($file);
// 追加新成员到文件
$current .= "John Smith\n";
// 将内容写回文件
file_put_contents($file, $current);
?>

至此,我们已将本次漏洞分析完毕,接下来还想说说关于该漏洞的一些细节。首先,这个漏洞要想利用成功,我们得知道缓存类所设置的键名,这样才能找到 webshell 路径;其次如果按照官方说明开发程序, webshell 最终会被写到 runtime 目录下,而官方推荐 public 作为 web 根目录,所以即便我们写入了 shell ,也无法直接访问到;最后如果程序有设置 $this->options['prefix'] 的话,在没有源码的情况下,我们还是无法获得 webshell 的准确路径

解释:

?username=mochazz123%0d%0a@eval($GET[]);//

这里的%0d%0a十六进制代表" \r \n "换行符, 而<?php\n//会将我们的内容都给注释掉,为了能跳出注释,这里使用换行,而我们输入最后的注释是要注释掉后边产生要闭合的' "; '

漏洞修复

官方的修复方法是:将数据拼接在 php 标签之外,并在 php 标签中拼接 exit() 函数。

现在明白刚开始提示的Cache success了。。。

相关推荐
找藉口是失败者的习惯1 小时前
Jetpack Compose 如何布局解析
android·xml·ui
Estar.Lee6 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
温辉_xh6 小时前
uiautomator案例
android
工业甲酰苯胺7 小时前
MySQL 主从复制之多线程复制
android·mysql·adb
少说多做3437 小时前
Android 不同情况下使用 runOnUiThread
android·java
Estar.Lee9 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯9 小时前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey11 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!12 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟13 小时前
Android音频采集
android·音视频