thinkphp getshell
ThinkPHP的Getshell,说白了就是找到一条能让你往服务器上写文件、并且还能执行它的路。这条路通常不是框架本身直接给的,而是开发者写代码时留下的缝。我见过太多案例,根子都在一个看似无害的函数上——file_put_contents,或者更隐蔽的fopen加fwrite。攻击者要做的,就是控制这些函数的第一个参数,也就是文件名或路径。
先看一个真实场景。某个基于ThinkPHP 5.0.24的CMS,后台有个“模板管理”功能,允许管理员编辑视图文件。开发者为了图方便,直接把用户提交的filename参数拼进了file_put_contents的路径里。代码大概长这样:
$content = $_POST['content']; $filename = $_POST['filename']; file_put_contents('./application/admin/view/' . $filename, $content); 这里没有任何过滤。你可以把filename设成../../public/shell.php,内容写个<?php @eval($_POST['cmd']);?>。请求一发,文件就落到Web目录了。ThinkPHP的目录结构是公开的,public是入口,所以这个路径完全能猜到。这种漏洞在早期ThinkPHP项目里一抓一大把,尤其是那些二次开发的系统,开发者根本不知道自己在写什么。
更隐蔽的一种,是利用ThinkPHP的缓存机制。框架会把一些配置或路由缓存写到runtime目录下,文件名通常由参数拼接而成。如果某个参数没过滤,你就能控制缓存文件的路径和内容。比如runtime/cache/下面,文件名可能是md5($key),但$key来自URL。有人试过把$key设成../../public/1.php,虽然md5后路径变了,但如果缓存驱动是File,且写入时没做路径规范化,照样能穿越。这种洞在ThinkPHP 3.2和5.0的某些版本里出现过,后来官方加了realpath检查才堵上。
还有个经典案例是think\cache\driver\File::set方法。它内部用file_put_contents写缓存,文件名由getCacheKey生成。如果getCacheKey里的options['prefix']或name可控,就能注入../。我见过一个项目,把用户ID直接当缓存key,结果id=../../public/shell,缓存文件就跑到Web目录了。内容呢?缓存内容通常是序列化的数组,但你可以控制数组里的某个值,比如把payload字段设成PHP代码。序列化后写进去,虽然外面有exit或?>干扰,但配合php://filter或者直接构造<?php在序列化字符串里,还是能执行。
ThinkPHP的I函数也出过事。早期版本里,I('post.')能获取全部POST数据,如果开发者用extract或者parse_str处理,就可能变量覆盖。有个漏洞是I('get.id')后面直接跟file_put_contents,但没过滤.和/。攻击者传id=../../public/1.php,再配合content参数,就写进去了。这种洞的利用链很短,不需要什么高级技巧,就是拼路径。
防御其实不复杂。别让用户控制文件路径,这是铁律。如果非要动态文件名,用basename取文件名,再用realpath检查最终路径是否在允许目录内。ThinkPHP官方后来在File驱动里加了checkPath,但很多老项目没升级。另外,runtime目录别放在Web根下,或者干脆禁用php在runtime里的执行权限。Nginx里加个location ~ /runtime/.*\.php$ { deny all; },能挡掉一大半。
我自己的经验是,拿到一个ThinkPHP站,先看版本,再翻runtime目录有没有写权限。如果runtime可写且能访问,直接找缓存写入点。有时候不用Getshell,光读缓存里的敏感信息就够喝一壶了。Getshell只是最后一步,前面的信息收集和路径猜测才是真功夫。别指望一个eval就搞定,现实里的洞往往需要两三个条件组合。









