从sql注入到rce——thinkphp3的sql注入nday的深度利用

有师傅发来了一道起初我看来是不可能完成的ctf题目,题目的功能很短,只有寥寥数行,是基于thinkphp3.2.3的一个开发的应用

说到thinkphp3,首先想到的就是sql注入了,但是题目对于mysql的配置非常严格

无法进行文件的读取和写入,同时采用了站库分离的设计,会让人觉得这个sql注入变得鸡肋起来,无法进一步的利用。tp3的反序列化才能达到客户端任意文件读的目的,这里显然也没满足这个要求。一时间让我感觉无从下手。

当时看了一段时间后,我认为这题目应该是没法做的,但是这里陷入了经验主义的陷阱。毕竟之前对于thinkphp3进行了较为详细的漏洞复现,觉得可能也就到这里了。没想到这边被我忽略的地方和强网杯当时被我忽略的地方如出一辙。看了别的师傅对于find函数的详细分析后(https://ml-hacker.github.io/2024/01/24/find-analysis/),我突然发现了思路,在find函数中也用到了S函数,那么这个S函数是之前出现了缓存getshell问题的缺陷。这里是不是会存在同样的问题,但是当时我走偏了一段,在分析S函数时,我看到了这样的写法

可以看到,这里我们可以实例化任意Think\Cache的子类,所以就狠自然而然地想到是不是可以从db的缓存方式入手(因为thinkphp3的注入是可以进行堆叠注入的,所以也可以创建我们需要的表)而db缓存方式中正好有个unserialize可以触发反序列化,达到后续的任意客户端文件读的目的,但是题目的mysql版本是mysql8,似乎网上目前的开源工具都无法直接利用,而是报数据格式错误的问题。似乎思路又进入了死胡同。

但是回想一下,对cache以及其中的get方法进行关注的原因是因为find函数中的这样设计,这边是从缓存中拿取数据。

那么既然有对数据的拿取操作,必然也有着对于数据的写入操作,在底下找到了之前被我忽视的数据写入缓存的操作

这边的$data不会进行任何的过滤操作,利用方式与之前的tp3缓存使用不当getshell方法利用相同。key的取值来自$options,而由于tp3的sql注入的where注入原理就在于污染了进行查询的$option数组,所以此处的$key,$data,$cache都可控,那么我们这就有了个可以写入任意目录,但是文件名为key的md5值的webshell了。如果是一般的网站,且web目录可写,那么可以直接在web目录下写入webhsell,或者直接访问缓存达到getshell的目的,但是这道题专门将tp的目录隔离在了web路径访问不到的地方,且web根目录无可写权限。

那么如何访问解析到我们的webshell就成了一个难题,毕竟权限有限,且文件名不可控。这里首先我对所有可以include的地方进行了检索,如加载配置文件,autoload等方法,但是发现在thinkphp核心类目录不可写的情况下,autoload寻找时会进行拼接.class.php这样我们的md5文件名是如何也找不到的,加载配置文件也是同理,唯一可能的是开启了多语言的切换功能,可以从语言那进行include,但是题目是默认配置,也是没有开启这个功能。既然这些路都走死了,可能问题还是在缓存上。

在代码审计时,我发现了一个很有趣的文件common~runtime.php,这个文件里面记录了所有thinkphp在运行时调用的类和函数,这时候我发现了一个很有趣的类以及它的run方法。

这个类的run方法,恰好也是使用了md5的content的值进行文件的命名,最后走到Storage::load其实也就是调用了文件包含的include方法。那么我们就有了思路,将cache的key的值构造成之前show函数中传入的base64_encode($content['content'])的值

同时将生成缓存文件路径即options['temp']的位置替换成C('CACHE_PATH') . $_data['prefix'],就可以完美的进行cache的覆盖,从而让server加载我们写入的webshell缓存,整个流程豁然开朗起来。

所以整体流程就分为这三步来进行,第一步获取base64_encode($content['content'])的值,第二步将该值作为键值通过sql注入写入webshell,第三步再次访问第一次的路由触发webshell。

image-20240126210426232

image-20240126210446145

image-20240126210607047

ParseTemplateBehavior类的调用路径,这里我一开始也没想到会被Hook这触发,所以看任何函数时都要仔细